深入理解Vue中的nextTick的机制

196 阅读3分钟

为什么使用nextTick

  • Vue的数据更新是采用延迟异步更新的,就是说当我们修改了数据之后,页面并不会马上就更新,如果这个时候我们通过DOM操作来获取数据的话,获取的还是之前的旧的数据,这个时候我们就可以使用$nextTick方法,因为这个方法知道什么时候DOM更新完成。

  • 原理在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

使用场景

在created 生命周期执行DOM操作

Vue中生命周期created函数执行时DOM其实并未进行渲染,这个时候可以将要进行DOM操作的代码放进this.$nextTick()的回调函数中,代码会在DOM节点更新完成后执行。总的来说,nextTick方法的作用就是让代码延迟执行

在数据变化后需要进行基于DOM结构的操作

在更新完数据后,如果还有操作要根据更新数据后的DOM结构进行,应当将这部分操作写入this.$nextTick()回调函数中

Vue.nextTick( [callback, context] )

  • 参数

    {Function}[callback] 回调函数,不传参数时提供promise调用

    {Object}[context] 回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上

     // 修改数据
    vm.msg = 'Hello'
    // DOM 还没有更新
    Vue.nextTick(function () {
    // DOM 更新了
    })
    
    // 作为一个 Promise 使用 
    Vue.nextTick()
    .then(function () {
    // DOM 更新了
    })
    

    Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例

实现原理

通过源码知道,timeFunc这个函数起延迟执行的作用,它有四种实现的方式:

  • Promise
  • MutationObserver
  • setImmediate
  • setTimeout
// 执行flushCallbacks ()方法遍历callbacks数组,依次执行callbacks里的每个函数
function flushCallbacks () {
  pending = false
  // 拷贝
  const copies = callbacks.slice(0)
  // 清空
  callbacks.length = 0
  // 遍历执行
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
// 降级策略
// 判断使用哪种异步回调方式
// 先执行任务优先级高的微任务
// 首先尝试使用Promise.then()——微任务
// 其次尝试使用MutationObserver()回调——微任务
// 再次尝试使用setImmediate()回调——宏任务
// 最后尝试使用setTimeout()回调——宏任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
   // 标记当前nextTick使用的是微任务
  isUsingMicroTask = true
  // 不是IE环境并且支持MutationObserver,
  // MutationObserver是一个用于监视DOM变动的接口,监听一个DOM对象上发生的子节点删除、属性修改、文本内容的修改
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  // 实例化一个MutationObserver 类
  const observer = new MutationObserver(flushCallbacks)
  // 创建一个文本节点
  const textNode = document.createTextNode(String(counter))
  // 监听文本节点,当数据发生变化的时候执行flushCallbacks
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    // 数据更新
    textNode.data = String(counter)
  }
  // 标记当前nextTick使用的是微任务
  isUsingMicroTask = true
  // 判断是否支持setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 如果都不支持则选择setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

每次调用nextTick函数时

  • 把传入的回调函数cb压入callbacks数组中
  • 执行timerFunc函数,延迟调用flushCallbacks函数
  • 遍历执行callbacks数组中的所有函数
/ 声明nextTick函数,接受一个回调函数和一个执行上下文为参数
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将传入的回调函数存放到数组当中后面会遍历执行其中的回调
  callbacks.push(() => {
    if (cb) { // 对传入的回调进行try catch 错误捕获
      try {
        cb.call(ctx)
      } catch (e) { // 进行统一的错误处理
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })

  // 如果没有pending的回调,就执行timerFunc函数选择当前的环境优先支持的异步方法
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  // 如果没有传入回调函数,并且当前环境也支持Promise,则返回一个Promise
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

原理总结

以上就是VUE的nextTick方法的实现原理总结一下:

  • vue用异步队列的方式来控制DOM更新和nextTick回调先后执行

  • MicroTask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕

  • 因为兼容性问题,vue不得不做了microtask向macrotask的降级方案