VUE源码系列三:nextTick原理解析(附源码解读)

964 阅读3分钟

来点鸡汤

男人一事无成时的温柔是最廉价的

前言

在理解nextTick之前,我们先要了解js的运行机制

js运行机制

js执行是单线程的,基于事件循环,事件循环大致分为以下几个步骤:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件,可以看出,这个任务队列主要存放异步任务

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 执行结束后,都要检查一遍micro task,并执行清理完成再进入下一轮事件循环。

传送门:segmentfault.com/a/119000001…

nextTick

它的源码很简单,
源码:src/core/util/next-tick.js

export let isUsingMicroTask = false
/* 存放异步执行的回调 */
const callbacks = []
/* 标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送 */
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]()
  }
}

/* 一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timeFunc将被调用 */
let timerFunc
/* 兼容Promise的浏览器 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
  /* 执行微任务标志位 */
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  /* 不支持Promise则使用MutationObserver */
  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)) {
  /* 不支持Promise和MutationObserver则使用setImmediate */
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  /* 否则都使用setTimeout */
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
/* 延迟一个任务使其异步执行,在下一个tick时执行,这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc,目的是延迟到当前调用栈执行完以后执行 */
export function nextTick (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)
    }
  })
  /* 已经有timerFunc被推送到任务队列中去则不需要重复推送 */
  if (!pending) {
    pending = true
    timerFunc()
  }
  /* nextTick没有传回调函数时,返回一个Promise,我们可以nextTick().then(() => {}) */
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

解析:可以看出,nextTick在内部对环境做了兼容,首先会检测是否支持微任务:Promise,支持的话则把我们传入的回调押入队列中在下一个tick中等待执行(flushCallbacks依次执行回调),若不支持则使用MutationObserver,如以上两者都不支持则使用宏任务:setImmediate和setTimeout。
最后如果我们没有给nextTick传入回调,则可以nextTick().then(() => {})跳到then逻辑中。

总结

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

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