# 【vue 源码系列】vm.$nextTick(cb)
# 官方描述
- 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
# 为什么会有 nextTick
- vue 采用的异步更新策略,当监听到数据发生变化的时候不会立即去更新 DOM,而是开启一个任务队列,并缓冲在同一事件循环中发生的所有数据变更。这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作 DOM 的次数。
- 那么这样就会导致在有的时候数据改掉了,但是它的更新异步的,而我在获取的时候,它还没有来得及改。
# nextTick 实现原理
- 将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;
vm.$nextTick(cb)
提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0)- 判断当前环境优先支持的异步方法,优先选择微任务
task 的执行优先级: Promise -> MutationObserver -> setImmediate -> setTimeout setTimeout 可能产生一个 4ms 的延迟,而 setImmediate 会在主线程执行完后立刻执行 setImmediate 在 IE10 和 node 中支持 当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次
# Vue 中 NextTick 源码如下(2.6 版本)
const callbacks = []; //回调函数
let pending = false; // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
//将 callbacks 复制一份出来然后清空,再遍历备份列表执行回调
function flushCallbacks() {
pending = false; //把标志还原为false
const copies = callbacks.slice(0); // 拷贝一份 callbacks
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
let timerFunc; //先采用微任务并按照优先级优雅降级的方式实现异步刷新
// 默认使用microtask(微任务)
let useMacroTask = false;
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (
// // 如果不支持 promise,就判断是否支持 MutationObserver
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
// MutationObserver 主要是监听dom变化 也是一个异步方法
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") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 以上三种都不支持就最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
// 回调的 this 自动绑定到调用它的实例上
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);
}
});
// 如果当前没有在 pending 的回调,
// 就执行 timeFunc 函数选择当前环境优先支持的异步方法
if (!pending) {
pending = true;
timerFunc();
}
// 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
// 在返回的这个 promise.then 中 DOM 已经更新好了,
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}