Vue源码学习3.4:nextTick

231 阅读3分钟

建议PC端观看,移动端代码错乱

先简单了解一下微任务和宏任务基础概念:

事件循环只有一个。macroTaskmicroTask 是一个大的任务容器,里面可以有多个任务队列。不同的任务源,任务会被放置到不同的任务队列。那任务源是什么呢,比如 setTimeoutsetImmediate,这都是不同的任务源,虽然都是在 macroTask 中,但肯定是放置在不同的任务队列中的。可以看下这篇文章

其伪代码大概就是这样

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

1. 选择异步方式

其实在 nextTick 中关于如何选择异步的实现是改变了好几次的:

  • @2.4 及之前的版本:Promise > MutationObserver > setTimeout
  • @2.5 的中间版本:
    • macroTimerFunc 实现宏任务:setImmediate > MessageChannel > setTimeout
    • microTimerFunc 实现微任务:Promise > 宏任务
    • 同时用 useMacroTask 来区分使用哪个函数,默认值是 false
  • @2.6 的最新版本:Promise > MutationObserver > setImmediate > setTimeout

有兴趣的话可以看看之前版本的介绍,这里就只介绍最新版本的实现:

// src/core/util/next-tick.js

let timerFunc
export let isUsingMicroTask = false

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true // 监听文本内容的修改
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

只用一个 timerFunc 变量用来保存异步的实现,优先级是 Promise > MutationObserver > setImmediate > setTimeout

2. nextTick

// src/core/util/next-tick.js
export let isUsingMicroTask = false
const callbacks = []
let pending = false

function flushCallbacks ({
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc
// ...实现优先级:Promise > MutationObserver > setImmediate > setTimeout

export function nextTick (cb?: Function, ctx?: Object{
  let _resolve
  // 将传入的函数包装一层,绑定作用域,并try-catch捕获错误
  // 如果没传入函数,且浏览器原生支持 Promise 的情况下,让 Promise resolve;
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // pending 是一个开关,每次执行 flushCallbacks 后,会将 pending 重置为 fasle
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 这里返回一个 Promise, 所以我们可以这样调用,$this.nextTick().then(xxx)
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

把传入的回调函数 cb 压入 callbacks 数组,最后一次性地执行 timerFunc,而它们都会在下一个 tick 执行 flushCallbacksflushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。

这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

比如这个例子:

new Vue({
    // 省略
    created() {
        // 执行第一个时,首先 fn1 会被 push 进 callbacks,再往下走
        // pending 为 false, 所以会进入 if (!pending),然后 pending 被设为true, 执行 timerFunc
        this.$nextTick(fn1);
        // 执行第二 个时,pending为true,这时就不会进入 if (!pending) 了,
        // 但是 callbacks.push 是会执行的,也就是说会把 fn2 push进 callbacks 数组
        this.$nextTick(fn2);
        // 同第二个
        this.$nextTick(fn3);
    }
})

这三个 this.$nextTick 执行完后,其实就相当于往 callbackspush 了三个 fn,在下次执行 timerFunc 时,flushCallbacks 内的代码才会执行,也就是执行我们传入的 fn

总结

通过这一节对 nextTick 的分析,并结合上一节的 setter 分析,我们了解到数据的变化到 DOM 的重新渲染是一个异步过程,发生在下一个 tick。这就是我们平时在开发的过程中,比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick 后执行。比如下面的伪代码:

getData(res).then(()=>{
  this.xxx = res.data
  this.$nextTick(() => {
    // 这里我们可以获取变化后的 DOM
  })
})

Vue.js 提供了 2 种调用 nextTick 的方式,一种是全局 API Vue.nextTick,一种是实例上的方法 vm.$nextTick,无论我们使用哪一种,最后都是调用 next-tick.js 中实现的 nextTick 方法。