建议PC端观看,移动端代码错乱
先简单了解一下微任务和宏任务基础概念:
事件循环只有一个。
macroTask和microTask是一个大的任务容器,里面可以有多个任务队列。不同的任务源,任务会被放置到不同的任务队列。那任务源是什么呢,比如setTimeout,setImmediate,这都是不同的任务源,虽然都是在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 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 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 执行完后,其实就相当于往 callbacks 内 push 了三个 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 方法。