在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看成微任务就可以了,这里就不展开说了

Logo

前往低代码交流专区

更多推荐