根据源码分析vue中nextTick的实现原理
从
Vue 2.x
源码( src/core/util/next-tick.js
)的角度,深入分析 nextTick
的实现原理。以下是关键代码逻辑的分步解析:
一. 核心变量定义
const callbacks = []; // 回调队列
let pending = false; // 是否正在等待异步执行的标记
let timerFunc; // 异步调度函数
callbacks
存储所有待执行的回调函数。pending
防止多次触发异步任务。timerFunc
根据不同环境动态赋值为最优的异步方法(微任务或宏任务)。
二. 异步策略选择(降级处理)
Vue
根据环境选择最高效的异步方式,优先级为 微任务 > 宏任务:
1. 微任务优先
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
// iOS UIWebView 的 Promise.then 可能不触发,强制宏任务兜底
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true; // 标记为微任务
}
- 优先使用
Promise.then
(微任务),确保回调在DOM
更新后立即执行。 - 针对
iOS UIWebView
的特殊修复(微任务可能不触发)。
2. 降级到 MutationObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1;
const textNode = document.createTextNode(String(counter));
const observer = new MutationObserver(flushCallbacks);
observer.observe(textNode, { characterData: true });
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter); // 触发 MutationObserver
};
isUsingMicroTask = true;
}
- 监听
DOM
节点变化触发微任务(非 IE 环境)。
3. 降级到宏任务
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => setImmediate(flushCallbacks);
} else {
timerFunc = () => setTimeout(flushCallbacks, 0);
}
setImmediate(Node.js)
或 setTimeout
(浏览器)兜底。
三、回调执行逻辑
function flushCallbacks() {
pending = false; // 重置标记,允许下次调度
const copies = callbacks.slice(0); // 复制队列,避免执行时新回调污染
callbacks.length = 0; // 清空原队列
for (let i = 0; i < copies.length; i++) {
copies:ml-search[i]; // 依次执行回调
}
}
- 复制队列:防止回调中再次调用
nextTick
导致死循环。 - 清空原队列:确保同一批回调只执行一次。
四、 nextTick 函数实现
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); // 支持 Promise 化调用
}
});
if (!pending) {
pending = true; // 标记为等待执行
timerFunc(); // 触发异步调度
}
// 支持 Promise 链式调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve; // 保存 resolve 函数供回调触发
});
}
}
- 将回调推入队列:用户传入的回调被包装后存入
callbacks
。 - 触发异步调度:若未在等待中,调用
timerFunc
异步执行回调。 Promise
支持:当不传cb
时返回Promise
,便于链式调用。
五、 与 Vue 更新流程的结合
在 Vue
的 异步更新队列 中,数据变化触发的 DOM
更新会被合并到同一事件循环:
// src/core/observer/scheduler.js
function queueWatcher(watcher: Watcher) {
const id = watcher.id;
if (!flushing) {
queue.push(watcher); // 将 Watcher 推入队列
}
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue); // 异步执行队列刷新
}
}
flushSchedulerQueue
:执行所有Watcher
的run
方法,更新DOM
。- 用户通过
nextTick
注册的回调 会在flushSchedulerQueue
之后执行,确保能访问最新DOM
。
六、关键设计思想
- 批量执行:同一事件循环内的所有数据变更和
nextTick
回调合并执行。 - 异步优先级:微任务优先(更快),宏任务兜底(兼容性)。
- 状态隔离:通过
pending
和队列复制避免递归调用问题。
this.message = 'updated';
Vue.nextTick(() => {
console.log('DOM updated:', this.$el.textContent); // 正确拿到最新 DOM
});
数据变更触发 Watcher
队列更新,nextTick
回调在队列刷新后执行。
七、实现原理总结
通过源码分析,nextTick 的核心实现逻辑是:
1. 队列管理:
合并同一事件循环内的回调。
2. 异步调度:
优先微任务(Promise/MutationObserver
),降级宏任务(setImmediate/setTimeout
)。
3. 执行顺序:
确保 DOM 更新队列先于用户回调执行。