Last updated on January 10, 2026 pm
不出意外这应该是2025年该公众号更新的最后一篇文章,在这里感谢大家的关注与陪伴。愿大家新的一年代码零 bug,接口全 200,升职加薪,一路开挂。
这是Vue从入门到精通系列文章的第10篇,在上一篇文章中讲了Vue中的nextTick的定义和几种使用场景,今天我们结合源码来说说nextTick的原理和实现。
nextTick 的源码在 Vue 项目的 /src/core/util/next-tick.js 里,核心逻辑不复杂,咱们拆解开来讲,不用怕看不懂。
核心变量和函数
首先有几个关键东西:
callbacks:就是咱们说的“异步操作队列”,所有通过 nextTick 传入的回调函数,都会被放进这个数组里。
pending:一个标识位,用来保证同一时间只执行一次异步任务,避免重复执行。
timerFunc:核心函数,用来决定用什么方式执行异步任务(会做降级处理)。
flushCallbacks:用来执行 callbacks 队列里所有回调函数的函数。
nextTick 核心函数逻辑
咱们先看 nextTick 函数的核心代码(保留关键逻辑,去掉冗余注释):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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); } });
if (!pending) { pending = true; timerFunc(); }
if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve; }); } }
|
逻辑很简单:
把我们传入的回调函数,包装一下放进 callbacks 队列;
如果当前没有正在执行的异步任务(pending 为 false),就调用 timerFunc 开启异步任务;
如果没传回调函数,就返回一个 Promise,这样就能用 async/await 了。
timerFunc 降级策略
timerFunc 是用来执行异步任务的,它会根据当前浏览器环境,选择最优先、性能最好的方式来执行,做了四层降级处理,顺序是:Promise.then > MutationObserver > setImmediate > setTimeout。
为什么要降级?因为不同浏览器对这些 API 的支持不一样,而且优先级也不同——微任务(Promise、MutationObserver)的优先级比宏任务(setImmediate、setTimeout)高,能更快执行,所以优先用微任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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) } }
|
flushCallbacks 执行回调队列
不管是用微任务还是宏任务,最终都会调用 flushCallbacks 函数,来执行 callbacks 队列里的所有回调。
1 2 3 4 5 6 7 8 9 10
| function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
|
这里有个小细节:为什么要复制一份 callbacks 再执行?因为在执行回调的过程中,可能会有新的 nextTick 被调用,要是直接操作原队列,会导致回调函数重复执行或者顺序错乱,复制一份就能避免这个问题。
简单总结
把 nextTick 传入的回调函数放进 callbacks 队列,然后根据浏览器环境选择最优的异步方式(微任务优先),等异步任务触发后,执行 flushCallbacks 函数,依次执行队列里的所有回调,这样就能保证回调函数里拿到的是更新后的 DOM 了。
【往期精彩】