☰
Vue.js 源码解析:nextTick 的异步更新原理与事件循环实现
2026/9/28 2:58:37 网站建设 项目流程
  • 文档
  • 教程
  • 前端

【免费下载链接】vue-analysis

:thumbsup: Vue.js 源码分析

项目地址:https://gitcode.com/gh_mirrors/vu/vue-analysis
点击查看免费下载

nextTick是 Vue.js 异步更新机制的核心基石:数据变化后 DOM 并不会立即更新,而是通过nextTick推迟到下一个 tick 统一执行渲染。本文基于 Vue 2.x 源码 next-tick.js 逐行剖析其实现,讲清事件循环、宏任务/微任务的选择逻辑、Vue.nextTick与vm.$nextTick两种调用方式,并给出在真实开发中依赖更新后 DOM 的实战方案。

JS 运行机制:事件循环与任务队列

Vue 的nextTick之所以能做到"延迟执行",本质上是借用了 JavaScript 运行时的异步调度机制。JS 执行是单线程的,基于事件循环(Event Loop)工作,大致分为以下几个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

主线程的一次执行过程就是一个 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 = false

next-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 }) } }

它的逻辑非常简洁:

  1. 把传入的回调cb包装后压入callbacks数组(注意用try/catch包裹,出错时走handleError(e, ctx, 'nextTick'),保证单个回调异常不会中断整个队列);
  2. 判断pending,为false时置为true,并根据useMacroTask条件选择执行macroTimerFunc()或microTimerFunc()——它们都会在下一个 tick 触发flushCallbacks;
  3. 最后一个 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 异步更新的完整闭环:

  1. 数据变化触发Watcher更新,queueWatcher将渲染 watcher 去重入队;
  2. nextTick(flushSchedulerQueue)把真正渲染动作推迟到下一个 tick;
  3. 下一个 tick 到来时,flushCallbacks批量执行回调队列,完成组件更新与 DOM patch;
  4. 因此,数据变化到 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 源码分析

项目地址:https://gitcode.com/gh_mirrors/vu/vue-analysis
点击查看免费下载

相关推荐

上一篇:Node-Redis数据序列化终极指南:JSON、MsgPack、Protocol Buffers性能对比与实战选择
下一篇:终极指南:5分钟掌握MediaCrawler多平台数据采集神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询