vue 问题笔记 ref获取不到指定的DOM节点问题解决

在Vue中使用ref获取DOM节点可能因节点未渲染好而失败,因为Vue更新DOM是异步的。可使用Vue.nextTick(callback)让方法在DOM节点解析后执行,其返回Promise对象。文章还解析了$nextTick的源码,包括初始化变量、异步执行(promise、MutationObserver、setTimeout)、返回函数,以及相关打印顺序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在vue中不建议操作DOM,但是如果我们在没办法的情况下,可以通过ref来进行DOM操作,而在使用DOM时,可能会有获取不到ref对应DOM节点的问题

像下面这种情况,这是执行了console.log(this.$refs)的情况

在用console打印出来的时候明明看到可以按下箭头显示我们要的组件内容,但是在花括号里面却写的是undefined,此时如果要获取这个节点的属性值,会报无法从undefined取值的错误。

之所以会这样,是因为vue在更新DOM是异步的,只要侦听到数据发生变化,Vue将开启一个队列,并缓存在同一事件循环中发生的所有数据变更。即是说,在我们更新数据时,组件不会立即重新渲染,而是在刷新队列时才进行渲染。

在上面代码中,DOM节点还没创建出来的时候,就执行了这个方法,导致无法正确地获取DOM节点,所以我们应该在DOM节点创建后才执行这个方法。

对于这个问题,其实只要做个异步处理就可以了,最简单的做法就是让这个方法延迟一点执行,使用一个setTimeout来将这个方法延迟执行。

setTimeout(function(){
    console.log(this.$refs);
},time)

虽然setTimeout可以解决这个问题,但是这个time我们无法确定,因为影响解析的因素很多,如果延迟的时间太长又可能造成其他问题,所以我们采用另一个方法:Vue.nextTick(callback)

Vue.nextTick(callback)可以让里面的方法在DOM节点全部解析后才执行。在组件内使用时可以直接使用this来代替Vue,this会自动绑定到当前的Vue实例上。

this.$nextTick(function(){
    console.log(this.$refs);
})

同时,因为$nextTick()返回一个Promise对象,所以可以使用async/await语法完成同样的事情。

await this.$nextTick();
console.log(this.$refs);

至此,问题解决


问题解决了,但是原理其实说的很浅,之前只是个小笔记,既然那么多人看,我就把相关的原理也写出来吧

$nextTick原理

上文说到了,我们使用ref获取DOM节点时,因为节点可能还没渲染好,所以我们无法正常获取到,那么vue内部的$nextTick是怎么做到在DOM节点渲染好后才去获取的呢

我们先稍微看一下源码

export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

  function nextTickHandler () {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  if (typeof Promise !== 'undefined' && isNative(Promise)) { // 使用promise
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      if (isIOS) setTimeout(noop)
    }
  } else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) { // 使用MutationObserver
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else { // 使用setTimeout
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

初始化变量

接下来,开始解析源码,从上到下,我们首先看到,其定义了三个变量

  const callbacks = [] // 缓存要执行的函数
  let pending = false // 是否正在执行
  let timerFunc // 保存要执行的函数

然后,在这里采用了闭包,在函数内创建了$nextTick真正调用的函数

  function nextTickHandler () {
    pending = false // 改为执行结束
    const copies = callbacks.slice(0) // 拷贝函数数组
    callbacks.length = 0 // 清空函数数组
    for (let i = 0; i < copies.length; i++) { // 执行函数数组中的函数
      copies[i]()
    }
  }

可以注意到,在上面源码中我在三处加了注释,正是$nextTick根据兼容性的不同,采取不同的措施,分别使用promise,MutationObserver和setTimeout来实现

异步执行

promise

  if (typeof Promise !== 'undefined' && isNative(Promise)) { // 使用promise
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      if (isIOS) setTimeout(noop)
    }
  }

这里采用了promise.then,将函数的执行延迟到函数调用栈的最末端,这里可以看成是将该函数放到微任务队列里,关于JavaScript的执行顺序,可见我的另一篇博客详解JavaScript异步执行顺序

MutationObserver

else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) { // 使用MutationObserver
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  }

MutationObserver接口提供了监视对DOM树所做更改的能力,当其监听的DOM发生改变,且所有DOM变动完成后,就会触发其回调函数,所以可以用在这里

当使用MutationObserver时,会创建一个文本节点,使用MutationObserver监听这个文本节点的变化。而在这段源码中,通过在内部写一个方法timerFunc来触发这个文本节点的变化,在调用$nextTick时,通过去调用timerFunc方法,来触发这个MutationObserver对应的回调函数,而因为DOM的改变是在同步代码执行完后执行,所以这样可以达到异步执行回调方法的目的

而这个MutationObserver的回调同样是微任务,所以可以替代promise.then,只是需要额外地去创建一个文本节点

setTimeout

 else { // 使用setTimeout
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

setTimeout其实很简单,只是单纯地将一个任务放到的任务队列的尾部

返回函数

return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }

在这里,将函数push进callbacks中,如果当前不是等待状态,就直接执行函数,最后一个promise是将函数做了一个promise化

相关的打印顺序

上面说到了源码中,如果promise可以使用,就会使用到promise.then,而MutationObserver可以使用时,也会用到相应的方法,而在这两种情况中,都是使用的微任务,所以就会出现下面的情况

click:function() {
  console.log('start')
  setTimeout(()=>{
    console.log('setTimeout1')
  },0)
  this.$nextTick(()=>{
    console.log('$nextTick1')
    setTimeout(()=>{
      console.log('setTimeout2')
    },0)
    this.$nextTick(()=>{
      console.log('$nextTick2')
    })
  })
  console.log('end')
}

当有这样的代码时,打印的结果是

start

end

nextTick1

nextTick2

setTimeout1

setTimeout2

相应的打印顺序理解,其实也很简单,只要我们将$nextTick看成微任务就可以了,这里就不展开说了

### 尚硅谷 Vue3 笔记 Markdown 文件 #### 1. Vue3 基础概念 Vue3 是基于虚拟 DOM 的渐进式 JavaScript 框架,提供了更高效的渲染机制和更好的开发体验。通过 `const VDOM = React.createElement('div', { id: 'app' }, 'Hello World')` 可以创建一个简单的虚拟 DOM 节点[^1]。 #### 2. 组件化开发 组件是 Vue 应用程序的核心构建单元。每个组件都有自己的数据、逻辑和视图层。可以使用 ES6 的类语法来定义组件: ```javascript class MyComponent extends Vue { constructor() { super(); this.message = "Hello from component!"; } } ``` #### 3. 插件系统 Vue 提供了一套灵活的插件系统用于扩展功能。可以通过如下方式定义并安装插件: ```javascript const myPlugin = { install(Vue, options) { // 添加全局过滤器 Vue.filter('capitalize', function (value) { if (!value) return ''; value = value.toString(); return value.charAt(0).toUpperCase() + value.slice(1); }); // 添加全局指令 Vue.directive('focus', { inserted(el) { el.focus(); } }); // 配置全局混入 Vue.mixin({ created() { console.log('Global Mixin Hook'); } }); // 添加实例方法 Vue.prototype.$myMethod = function () { console.log('Instance method called!'); }; } }; ``` #### 4. Composition API Composition API 是 Vue3 中引入的新特性之一,允许开发者更好地组织代码结构。它使得状态管理更加直观清晰,并且能够轻松实现跨生命周期钩子的状态共享。 ```javascript import { ref, reactive } from 'vue'; export default { setup() { const count = ref(0); const state = reactive({ name: 'Vue.js' }); function increment() { count.value++; } return { count, state, increment }; } }; ``` #### 5. Teleport 功能 Teleport 特性可以让组件的内容被移动到 DOM 结构中的其他位置而不影响其行为表现形式。这对于模态框等场景非常有用。 ```html <template> <teleport to="body"> <!-- Modal content --> </teleport> </template> ``` #### 6. Fragment 支持 现在可以在单个根节点之外返回多个顶级元素作为模板的一部分,这被称为片段支持。这样可以使某些布局设计变得更加简单自然。 ```html <template> <header>Header Content</header> <main>Main Section</main> <footer>Footer Area</footer> </template> ```
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值