Vue 的 nextTick 通过维护回调队列和异步调度机制,确保用户回调在 DOM 更新后执行。其核心是利用微任务的高优先级特性,结合降级策略实现跨平台兼容。例如,当我们修改数据后,Vue 会将 DOM 更新和用户定义的回调放入同一异步任务中,避免多次渲染。
废话不多说先直接上vue源码节选再做总结
// 存储所有的回调函数
const callbacks = [];
/* 类似于节流的标记位,标记是否处于节流状态。防止重复推送任务 */
let pending = false;
/* 遍历执行数组 callbacks 中的所有存储的 cb 回调函数 */
function flushCallbacks() {
// 重置标记,允许下一个 nextTick 调用
pending = false;
/* 执行所有 cb 回调函数 */
for (let i = 0; i < callbacks.length; i++) {
callbacks[i](); // 依次调用存储的回调函数
}
// 清空回调数组,为下一次调用做准备
callbacks.length = 0;
}
// 判断最终支持的 API:Promise / MutationObserver / setImmediate / setTimeout
let timerFunc;
if (typeof Promise !== "undefined") {
// 创建一个已resolve的 Promise 实例
var p = Promise.resolve();
// 定义 timerFunc 为使用 Promise 的方式调度 flushCallbacks
timerFunc = () => {
// 使用 p.then 方法将 flushCallbacks 推送到微任务队列
p.then(flushCallbacks);
};
} else if (
typeof MutationObserver !== "undefined" &&
MutationObserver.toString() === "[object MutationObserverConstructor]"
) {
/* 新建一个 textNode 的 DOM 对象,用 MutationObserver 绑定该 DOM 并指定回调函数。
在 DOM 变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),
即 textNode.data = String(counter) 时便会加入该回调 */
var counter = 1; // 用于切换文本节点的值
var observer = new MutationObserver(flushCallbacks); // 创建 MutationObserver 实例
var textNode = document.createTextNode(String(counter)); // 创建文本节点
observer.observe(textNode, {
characterData: true, // 监听文本节点的变化
});
// 定义 timerFunc 为使用 MutationObserver 的方式调度 flushCallbacks
timerFunc = () => {
counter = (counter + 1) % 2; // 切换 counter 的值(0 或 1)
textNode.data = String(counter); // 更新文本节点以触发观察者
};
} else if (typeof setImmediate !== "undefined") {
/* 使用 setImmediate 将回调推入任务队列尾部 */
timerFunc = () => {
setImmediate(flushCallbacks); // 将 flushCallbacks 推送到宏任务队列
};
} else {
/* 使用 setTimeout 将回调推入任务队列尾部 */
timerFunc = () => {
setTimeout(flushCallbacks, 0); // 将 flushCallbacks 推送到宏任务队列
};
}
function nextTick(cb) {
// 用于存储 Promise 的解析函数
let _resolve;
// 将回调函数 cb 添加到 callbacks 数组中
callbacks.push(() => {
// 如果有 cb 回调函数,将 cb 存储到 callbacks
if (cb) {
cb();
} else if (_resolve) {
// 如果参数 cb 不存在,则保存 Promise 的成功回调 resolve
_resolve();
}
});
// 第一次使用 nextTick 时,pending 为 false,下面的代码才会执行
if (!pending) {
// 改变标记位的值,如果有 nextTickHandler 被推送到任务队列中去则不需要重复推送
pending = true;
// 调用 timerFunc,将 flushCallbacks 推送到合适的任务队列
timerFunc(flushCallbacks);
}
// 如果没有 cb 且环境支持 Promise,则返回一个 Promise
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
// 保存 resolve 到 callbacks 数组中
_resolve = resolve;
});
}
}
核心思路总结:
- 核心机制:
- 维护
callbacks数组存储所有回调函数。 - 通过
flushCallbacks一次性执行所有回调。 timerFunc负责将flushCallbacks推入异步队列(根据环境选择微任务/宏任务)。
- 维护
- 调用流程:
this.$nextTick(cb)将回调加入callbacks。- 首次调用触发
timerFunc安排异步任务。 - 后续调用仅追加回调到
callbacks。 - 支持 Promise 语法(无回调时返回 Promise)。
- 流程图关键点:
- 回调收集 → 异步任务调度 → 批量执行。
补充知识点:
- 异步任务优先级:
- Vue 优先使用微任务(如
Promise、MutationObserver),确保在 DOM 渲染前执行回调。 - 降级机制:在不支持微任务的环境(如 IE11)下,依次降级到
setImmediate或setTimeout(宏任务)。
- Vue 优先使用微任务(如
- 事件循环中的执行时机:
// 微任务场景(默认) timerFunc = () => Promise.resolve().then(flushCallbacks) // 宏任务场景(降级) timerFunc = () => setTimeout(flushCallbacks, 0)- 微任务会在当前事件循环的 本轮末尾 执行,早于 UI 渲染。
- 宏任务会在下一轮事件循环执行,可能导致回调延迟到渲染后。
- 回调队列的锁定与清空:
- 执行
flushCallbacks前会 复制并清空callbacks数组,避免递归调用导致无限循环。function flushCallbacks() { const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
- 执行
- 与 Vue 更新机制的关系:
- Vue 的异步更新队列(
queueWatcher)同样依赖nextTick,确保所有数据变更后的 DOM 更新在一次异步任务中完成。 - 示例:修改数据后立即获取 DOM 状态,需通过
nextTick等待更新完成:this.message = 'updated' this.$nextTick(() => { console.log(document.getElementById('text').innerHTML) // 正确获取最新 DOM })
- Vue 的异步更新队列(
- Promise 语法支持:
- 当不传回调时,返回 Promise 对象,支持
async/await:await this.$nextTick() // 后续代码可访问更新后的 DOM
- 当不传回调时,返回 Promise 对象,支持
完整实现流程图:
graph TD
A[调用 this.$nextTick(callback)] --> B{是否有回调函数?}
B -->|是| C[将 callback 加入 callbacks 数组]
B -->|否| D[返回 Promise 实例]
C --> E{是否首次调用?}
E -->|是| F[调用 timerFunc 安排异步任务]
E -->|否| G[等待当前异步任务执行]
F --> H[根据环境选择微任务/宏任务]
H --> I[将 flushCallbacks 加入任务队列]
I --> J[事件循环执行 flushCallbacks]
J --> K[复制并清空 callbacks]
K --> L[依次执行所有回调]
D --> M[Promise.then 触发 flushCallbacks]
- THE END -
最后修改:2025年3月19日
共有 0 条评论