Vue 的异步更新机制

Vue 的异步更新机制

💡 原文中文,约6000字,阅读约需15分钟。
📝

内容提要

Vue 的异步更新机制以队列为核心:数据变更触发 setter 后,watcher 不立即更新,而是由 queueWatcher 去重后加入队列,并通过 nextTick(flushSchedulerQueue) 推迟到下一 tick 执行。flushSchedulerQueue 会先排序队列,确保父组件先于子组件、用户 watcher 先于渲染 watcher,再逐个运行。nextTick 利用 setImmediate、MessageChannel、Promise 或 setTimeout 实现异步,$nextTick 是其别名。

🔎

延伸解读

异步更新如何避免重复渲染

文章指出,连续修改多个响应式数据时,如果每次赋值都立即触发渲染,会导致页面卡顿。Vue 通过 queueWatcher 将 watcher 去重后加入队列,并借助 nextTick 推迟到下一 tick 统一执行。这样,同一 watcher 在一次更新中只运行一次,从而减少不必要的 render 和 patch,提升性能。

队列排序的隐藏规则

flushSchedulerQueue 会先对队列按 watcher.id 排序,确保父组件先于子组件更新,用户定义的 watcher 先于渲染 watcher 执行。这解释了为什么在 watch 回调中无法立即获取最新 DOM:渲染 watcher 总在最后运行,且与用户 watcher 处于同一 tick。若需访问更新后的 DOM,应使用 $nextTick。

nextTick 的降级策略

Vue 的 nextTick 根据环境选择异步方案:优先 setImmediate,其次 MessageChannel,再次 Promise,最后 setTimeout。文章提到,早期使用微任务(如 Promise)会导致更新时机不符合预期,因此改为宏任务优先。MessageChannel 的兼容性甚至优于 Promise,在 IE10 及以上可用。

两个队列的协作与区别

文章强调,Vue 异步更新涉及两个队列:queueWatcher 维护的 watcher 队列和 nextTick 维护的 callbacks 队列。watcher 队列在 flushing 时仍可插入新 watcher,以便及时处理更新;而 callbacks 队列在 waiting 为 true 时不会新增任务,通常只保持一个函数在排队。两者协作完成异步更新流程。

❓

Q&A

Vue 为什么需要异步更新机制?

Vue 采用异步更新是为了避免每次数据变更都立即触发视图重新渲染。如果连续多次修改数据(如 this.a=1; this.b=2; this.c=3),同步更新会导致多次不必要的渲染,性能开销大。异步更新将需要执行的更新函数先存入队列,去重后推迟到下一个 tick 统一执行,从而减少渲染次数,提升性能。

Vue 的 queueWatcher 是如何工作的?

queueWatcher 接收一个 watcher,首先检查其 id 是否已在 has 对象中,若未加入则标记 has[id]=true。如果当前未在 flushing 状态,直接将 watcher 推入 queue;如果正在 flushing,则根据 id 大小将 watcher 插入到队列中当前运行位置之后,确保顺序。最后,如果 waiting 为 false,则设置 waiting=true 并调用 nextTick(flushSchedulerQueue),将队列处理推迟到下一个 tick。

flushSchedulerQueue 函数的作用是什么?

flushSchedulerQueue 负责处理 queueWatcher 编排好的 watcher 队列。它首先将 flushing 置为 true,然后对队列按 watcher.id 排序,以保证父组件先于子组件更新、用户 watcher 先于渲染 watcher 执行。接着遍历队列,依次执行每个 watcher 的 run 方法,并从 has 中移除已处理的 watcher。最后重置调度状态,并调用 activated 和 updated 钩子。

Vue 的 nextTick 是如何实现异步的?

nextTick 通过一个立即执行函数创建闭包,内部维护 callbacks 数组和 pending 标志。它根据环境选择异步方法:优先使用 setImmediate,其次 MessageChannel,再次 Promise,最后降级为 setTimeout 0。当调用 nextTick(cb) 时,将回调推入 callbacks,若 pending 为 false 则设置 pending=true 并调用 timerFunc 在下一个 tick 执行所有回调。

在 watch 回调中能立即获取最新的 DOM 吗?

不能。因为 watch 回调与组件渲染 watcher 运行在同一个 tick 中,且 flushSchedulerQueue 会按 id 排序,组件渲染 watcher 的 id 通常更大,所以渲染 watcher 总是在用户 watcher 之后执行。因此,在 watch 回调执行时,DOM 尚未更新,无法获取最新的 DOM。

$nextTick 和 nextTick 有什么区别?

它们本质上是同一个函数。Vue 在原型上定义了 $nextTick,其内部直接调用 nextTick(fn, this),因此 $nextTick 只是 nextTick 的一个别名,方便在组件实例上使用。

🏷️

标签

➡️

继续阅读