- 文档
- 教程
- 前端
【免费下载链接】vue-analysis
:thumbsup: Vue.js 源码分析
nextTick是 Vue.js 异步更新机制的核心基石:数据变化后 DOM 并不会立即更新,而是通过nextTick推迟到下一个 tick 统一执行渲染。本文基于 Vue 2.x 源码 next-tick.js 逐行剖析其实现,讲清事件循环、宏任务/微任务的选择逻辑、Vue.nextTick与vm.$nextTick两种调用方式,并给出在真实开发中依赖更新后 DOM 的实战方案。
JS 运行机制:事件循环与任务队列
Vue 的nextTick之所以能做到"延迟执行",本质上是借用了 JavaScript 运行时的异步调度机制。JS 执行是单线程的,基于事件循环(Event Loop)工作,大致分为以下几个步骤:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
主线程的一次执行过程就是一个 tick,而所有的异步结果都是通过"任务队列"来调度。消息队列中存放的是一个个的任务(task),规范中规定 task 分为两大类,分别是macro task(宏任务)和micro task(微任务),并且每个 macro task 结束后,都要清空所有的 micro task。两者的执行关系可以用下面的伪代码描述:
for (macroTask of macroTaskQueue) { // 1. Handle current MACRO-TASK handleMacroTask(); // 2. Handle all MICRO-TASK for (microTask of microTaskQueue) { handleMicroTask(microTask); } }在浏览器环境中:
- 常见的macro task有
setTimeout、MessageChannel、postMessage、setImmediate; - 常见的micro task有
MutationObserver和Promise.then。
理解这个调度模型是理解nextTick的前提:nextTick要做的,就是把回调"塞进"下一个 tick 再执行,而具体通过哪种异步 API 实现,取决于环境支持情况——这正是 Vue 源码中一段复杂能力检测要解决的问题。
Vue 的实现:next-tick.js 源码逐段解析
在 Vue 2.5+ 中,nextTick的实现被单独抽到一个 JS 文件维护,总共 100 多行,位于 vue/src/core/util/next-tick.js。下面按源码结构分段剖析。
1. 回调队列与 pending 标志
import { noop } from 'shared/util' import { handleError } from './error' import { isIOS, isNative } from './env' 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]() } }callbacks是一个数组,用于收集同一 tick 内的所有nextTick回调;pending用于标记当前是否已经有异步任务在排队。flushCallbacks在下一个 tick 被触发时执行,它的逻辑非常简单:先把pending复位,然后slice(0)拷贝一份回调列表并清空原数组,最后遍历拷贝逐一遍历执行。
这里有两个值得注意的细节:
- 先拷贝再清空:执行回调期间如果有新的
nextTick被调用,新回调会进入新的callbacks,等待下一轮 flush,避免在同一轮 flush 中出现无限循环或顺序错乱; - 用数组而非每次单独调度:保证在同一个 tick 内多次执行
nextTick不会开启多个异步任务,而是把这些回调全部压成一个同步任务,在下一个 tick 一次性执行完毕。
2. 宏任务与微任务的降级链
let microTimerFunc let macroTimerFunc let useMacroTask = falsenext-tick.js声明了microTimerFunc和macroTimerFunc两个变量,分别对应 micro task 与 macro task 的延迟执行函数。源码注释(对应 next-tick.js)交代了设计背景:
在 < 2.4 版本中,Vue 处处使用 microtask,但有些场景下 microtask 优先级太高,会在本应顺序执行的事件之间触发(如 #4521、#6690),甚至会在同一事件的冒泡过程中触发(#6566);而处处使用 macro task 也会在 repaint 前状态刚被修改时产生微妙问题(如 #6813、out-in transitions)。因此 Vue 默认使用 microtask,同时暴露一种在需要时强制使用 macro task 的方式(如 v-on 绑定的事件处理函数)。
macro task 的实现(next-tick.js)遵循一条降级链:
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } }- 优先
setImmediate:这是高版本 IE 和 Edge 才支持的特性,也是"理论上的理想选择"——回调会在同一事件循环中所有 DOM 事件之后稳定排队; - 其次
MessageChannel:这是目前唯一能保证"在同一轮所有 DOM 事件触发之后一致地排队回调"的 polyfill 方案; - 最后降级
setTimeout 0:最通用的兜底方案,任何环境都可用。
注意这里使用了isNative检测(定义见 env.js),即typeof Ctor === 'function' && /native code/.test(Ctor.toString()),确保使用的是原生实现而非用户注入的 polyfill。
micro task 的实现(next-tick.js)则检测浏览器是否原生支持Promise:
if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { p.then(flushCallbacks) // 在问题 UIWebViews 中,Promise.then 不会完全失效, // 但可能卡在“回调已推入微任务队列但队列未被冲刷”的怪异状态, // 直到浏览器需要处理其他工作(如定时器)。因此通过添加一个空定时器来“强制”冲刷微任务队列。 if (isIOS) setTimeout(noop) } } else { // fallback to macro microTimerFunc = macroTimerFunc }如果不支持原生Promise,micro task 的实现会直接降级指向 macro task 的实现。另外针对 iOS 有一个特殊处理:在isIOS环境下追加一个空setTimeout(noop),用于强制冲刷可能卡住的微任务队列(isIOS的定义见 env.js,通过 UA 匹配iphone|ipad|ipod|ios或 weex 平台判断)。这是一个非常经典的"环境打补丁"写法。
3. nextTick 函数:入队与统一调度
next-tick.js对外暴露的第一个核心函数就是nextTick(next-tick.js):
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 if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }它的逻辑非常简洁:
- 把传入的回调
cb包装后压入callbacks数组(注意用try/catch包裹,出错时走handleError(e, ctx, 'nextTick'),保证单个回调异常不会中断整个队列); - 判断
pending,为false时置为true,并根据useMacroTask条件选择执行macroTimerFunc()或microTimerFunc()——它们都会在下一个 tick 触发flushCallbacks; - 最后一个 if 是Promise 化:当
nextTick不传cb时返回一个 Promise,比如nextTick().then(() => {}),当_resolve函数被执行时,就会跳到then的逻辑中。
为什么用 callbacks 数组而不是直接执行回调?原因在前面已经提到:保证同一个 tick 内多次执行
nextTick不会开启多个异步任务,而是把多次调用压成一个异步任务,在下一个 tick 统一执行完毕,极大减少浏览器调度开销。
4. withMacroTask:强制走宏任务的包装器
next-tick.js还对外暴露了第二个函数withMacroTask(next-tick.js):
export function withMacroTask (fn: Function): Function { return fn._withTask || (fn._withTask = function () { useMacroTask = true const res = fn.apply(null, arguments) useMacroTask = false return res }) }它是对函数做一层包装:在函数执行期间把useMacroTask置为true,确保函数执行过程中对数据做任意修改、触发响应式更新进而调用nextTick时,强制走macroTimerFunc;函数执行完毕后再复位为false。
为什么需要它?最典型的应用场景是DOM 交互事件。在 vue/src/platforms/web/runtime/modules/events.js 中,v-on绑定的事件回调在add阶段就被包装:
handler = withMacroTask(handler)也就是说,在事件回调(如click)内修改数据触发的更新,会以 macro task 调度,避免 microtask 高优先级导致的状态更新穿插在 DOM 事件冒泡过程中、或发生在 repaint 之前产生渲染异常——这正是源码注释中提到的 #6566、#6813 等历史 issue 的教训。
nextTick 与 Vue 的异步渲染链路
nextTick不是孤立存在的工具函数,它是 Vue响应式系统 → 异步批量更新 → DOM 重渲染整条链路的关键一环。
当数据被修改时,Watcher会被触发,而负责把渲染 Watcher 收集起来的queueWatcher(定义在 vue/src/core/observer/scheduler.js)会在末尾执行:
// queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) }也就是说,数据变化并不会立即重渲染 DOM,而是先把flushSchedulerQueue(真正执行组件更新、重新渲染和 patch 的函数)通过nextTick排入队列,统一到下一个 tick 执行。这正是上一节 setter 分析中"数据变化到 DOM 重新渲染是一个异步过程"的落点。
结合 scheduler.js 中queueWatcher的has[id]去重逻辑可以推断:即使同一 tick 内多次修改同一个 watcher 依赖的数据,也只会入队一次,配合nextTick的批量 flush,从机制上避免了无谓的重复渲染。
两种调用方式:Vue.nextTick 与 vm.$nextTick
Vue.js 提供了 2 种调用nextTick的方式,无论使用哪一种,最后都殊途同归地调用next-tick.js中实现的nextTick方法:
全局 APIVue.nextTick,在 vue/src/core/global-api/index.js 中直接挂载:
Vue.nextTick = nextTick实例方法vm.$nextTick,在 vue/src/core/instance/render.js 的renderMixin中定义:
Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) }注意vm.$nextTick与Vue.nextTick的差别:前者额外传入了当前组件实例this作为ctx,回调执行时this指向该组件实例,因此在组件内写this.$nextTick(...)时回调里的this就是组件本身,使用起来更顺手。
开发实战:数据更新后如何拿到新 DOM
理解了异步渲染机制,就理解了一个高频开发场景:从服务端接口获取数据并修改数据后,如果某些方法依赖数据修改后的 DOM,就必须在nextTick后执行。比如下面的伪代码:
getData(res).then(() => { this.xxx = res.data this.$nextTick(() => { // 这里我们可以获取变化后的 DOM }) })在实际项目中,常见用例包括:修改数据后立即读取元素尺寸(offsetHeight、getBoundingClientRect)、操作v-for新渲染出的 DOM 节点、与第三方库(图表、滚动条等)做 DOM 对接等。只要目标是"数据变化后的 DOM 状态",就应把代码放进$nextTick回调。
此外,nextTick的 Promise 化写法同样可以在实战中直接使用:
await this.$nextTick() // 到这里 DOM 已更新不传回调时它返回一个 Promise,配合async/await可以让异步渲染等待逻辑更扁平。
测试用例佐证
仓库的单元测试 vue/test/unit/modules/util/next-tick.spec.js 直接验证了上述行为:
- 传入回调时
nextTick(done)会在下一个 tick 触发回调; - 传入回调时返回
undefined; - 不传回调时返回 Promise,且
nextTick().then(done)正常工作; - 传入 falsy 回调加对象
ctx时,Promise 的 resolve 值就是该ctx对象; - 回调与 Promise 化调用共存时,回调先于 Promise resolve 执行。
这些断言与源码实现一一对应,也为我们验证nextTick语义提供了现成的参考。
总结
通过这一节对nextTick的分析,并结合 setter 分析,可以得出 Vue 异步更新的完整闭环:
- 数据变化触发
Watcher更新,queueWatcher将渲染 watcher 去重入队; nextTick(flushSchedulerQueue)把真正渲染动作推迟到下一个 tick;- 下一个 tick 到来时,
flushCallbacks批量执行回调队列,完成组件更新与 DOM patch; - 因此,数据变化到 DOM 的重新渲染是一个异步过程,发生在下一个 tick。
在实现层面,nextTick通过能力检测在Promise.then(默认 micro task)与setImmediate/MessageChannel/setTimeout(macro task)之间优雅降级,并用withMacroTask在v-on事件等特殊场景强制切换为 macro task,兼顾了性能、兼容性与渲染时序的正确性。掌握这份实现,不仅能用好Vue.nextTick/vm.$nextTick,也能在阅读响应式源码(scheduler.js、watcher.js)时理解每一次渲染背后的调度逻辑。
- 文档
- 教程
- 前端
【免费下载链接】vue-analysis
:thumbsup: Vue.js 源码分析
相关推荐
react-router-redux源码中的事件循环:理解异步路由更新
react router redux源码中的事件循环:理解异步路由更新 你是否曾在开发单页应用时遇到路由跳转后Redux状态未同步的问题?是否好奇用户点击链接到
前端状态管理路由Chatbox 聊天记录去哪了?3 分钟看懂它的本地存储方案
Chatbox 聊天记录去哪了?3 分钟看懂它的本地存储方案 重装系统、换电脑、甚至误删安装目录,AI 对话记录最怕的就是跟着一起消失。Chatbox 走的是本
AI 应用桌面应用大模型BunnyPHP错误处理与异常管理:构建健壮的消息队列应用
BunnyPHP错误处理与异常管理:构建健壮的消息队列应用 在构建基于消息队列的分布式系统时, BunnyPHP错误处理与异常管理 是确保应用稳定性的关键。作为
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考