本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途。
nextTick 是 Vue 的一个核心实现,在介绍 Vue 的 nextTick 之前,为了方便大家理解,先简单介绍一下 JS 的运行机制。
JS 运行机制
JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
-
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
-
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
-
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
-
主线程不断重复上面的第三步。
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 "任务队列" 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。
关于 macro task 和 micro task 的概念,简单通过一段代码演示他们的执行顺序:
for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();
// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 micro task 有 MutationObsever 和 Promise.then。
Vue 的实现
在 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
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]'
)) {
// 如果浏览器支持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)) {
// 如果浏览器上面两个都不支持,那么就退而求其次采用setImmediate、setTimeout这种宏任务
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
// 利用try catch来捕获错误,如果不捕获当回调函数错误的时候,整个线程就会崩溃,因为js是单线程
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 支持nextTick采用promise的方法
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
既然要实现异步,现在有两种方式,一种是采用微任务,一种是采用宏任务,现在大部分浏览器都支持Promise,所以nextTcik是一种微任务的异步方式,这比宏任务执行时机要早,所以性能更好。
next-tick.js 对外暴露了 nextTick函数,这就是我们在上一篇文章中执行 nextTick(flushSchedulerQueue) 所用到的函数。它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,最后一次性地执行timerFunc(),而它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
nextTick 函数最后还有一段逻辑:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
这是当 nextTick 不传 cb 参数的时候,提供一个 Promise 化的调用,比如:
nextTick().then(() => {})
当 _resolve 函数执行,就会跳到 then 的逻辑中。
当我们修改一个响应式数据之后:
data() {
return {
msg: 'hello world'
}
}
change() {
this.msg = 'hello vue'
console.log('sync:', this.$refs.msg.innerText) // hello world
this.$nextTick(() => {
console.log('async:', this.$refs.msg.innerText) // hello vue
})
this.$nextTick().then(() => {
console.log('async promise:', this.$refs.msg.innerText) // hello vue
})
}
如果跳转下$nextTick的位置,如下:
change() {
this.$nextTick(() => {
console.log('async:', this.$refs.msg.innerText) // hello world
})
this.msg = 'hello vue'
console.log('sync:', this.$refs.msg.innerText) // hello world
this.$nextTick().then(() => {
console.log('async promise:', this.$refs.msg.innerText) // hello vue
})
}
可以看到第一个$nextTick打印仍然是旧的值,这是因为在nextTick里面也是通过队列callbacks按照顺序执行的,谁先push到callbacks就先执行,它比this.msg = 'hello vue'后,把更新函数push到callbacks的时机要早,所以仍然是旧的值。
通过这一篇文章对 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 方法。