Vue 的异步更新机制

Vue 的异步更新机制

💡 原文约2000字/词,阅读约需8分钟。
📝

内容提要

Vue 的异步更新机制以队列为核心:数据连续赋值时,为避免每次更新都触发渲染,Vue 将 watcher 存入队列,通过 queueWatcher 去重,再用 nextTick(flushSchedulerQueue) 异步执行。flushSchedulerQueue 按 id 排序后运行 watcher。nextTick 借助 setImmediate、MessageChannel、Promise 或 setTimeout 创建异步任务,实现批量更新。

🔎

延伸解读

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

Vue 将数据变化触发的 watcher 先存入队列,通过 queueWatcher 去重,确保同一 watcher 只入队一次。随后用 nextTick 异步执行 flushSchedulerQueue,批量运行 watcher。这样连续赋值不会立即触发多次渲染,而是合并到一次更新中,减少不必要的 DOM 操作,提升性能。

flushSchedulerQueue 的排序与执行顺序

flushSchedulerQueue 会按 watcher.id 排序,保证父组件先于子组件更新,用户定义的 watcher 先于渲染 watcher 执行。排序后依次运行 watcher,并在过程中处理新加入的 watcher。因此,在 watcher 回调中不能假设能获取到最新的 DOM,因为渲染 watcher 可能尚未执行。

nextTick 的异步任务选择策略

nextTick 内部根据环境选择异步任务创建方式:优先 setImmediate,其次 MessageChannel,再次 Promise,最后 setTimeout。早期 Vue 使用微任务(如 Promise)导致更新时机不当,后改为宏任务优先。MessageChannel 在 IE 10+ 可用,兼容性较好,而 Promise 主要用于 Weex 等非 DOM 环境。

两个队列的协作与区别

Vue 异步更新涉及两个队列:queueWatcher 管理的 watcher 队列和 nextTick 的 callbacks 队列。watcher 队列负责收集需要更新的 watcher,callbacks 队列则存放 nextTick 回调。当 waiting 为 true 时,不会重复添加 callbacks,确保同一 tick 内只执行一次异步刷新。两者协作实现高效的批量更新。

❓

Q&A

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

Vue 采用异步更新是为了避免在数据连续赋值时频繁触发渲染,导致页面性能下降。通过将 watcher 存入队列并异步执行,可以批量更新,减少不必要的渲染。

Vue 的异步更新机制中,queueWatcher 函数的作用是什么?

queueWatcher 函数负责将需要更新的 watcher 添加到队列中,并进行去重(通过 has 对象)。如果队列尚未开始刷新,则直接推入队列;如果正在刷新,则按 id 顺序插入到合适位置。最后,如果尚未等待,则调用 nextTick(flushSchedulerQueue) 安排异步刷新。

flushSchedulerQueue 函数是如何处理 watcher 队列的?

flushSchedulerQueue 首先将 flushing 设为 true,然后按 watcher.id 排序队列,以确保父组件先于子组件执行、用户 watcher 先于渲染 watcher。接着遍历队列,依次执行每个 watcher 的 run 方法,并清除 has 中的记录。最后重置调度状态,并调用 activated 和 updated 钩子。

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

nextTick 通过一个立即执行函数创建闭包,内部维护 callbacks 数组和 pending 状态。它根据环境选择不同的异步方法:setImmediate、MessageChannel、Promise 或 setTimeout,将 nextTickHandler 推入异步任务队列。当调用 nextTick(cb) 时,会将回调加入 callbacks,并在 pending 为 false 时触发 timerFunc,从而在下一个 tick 执行所有回调。

Vue 的 nextTick 使用了哪些异步 API?它们的选择顺序是什么?

Vue 的 nextTick 按以下顺序选择异步 API:1. setImmediate(如果原生支持);2. MessageChannel(如果支持,包括 PhantomJS 的特定情况);3. Promise(用于非 DOM 环境,如 Weex);4. setTimeout(降级方案,用于 IE10 及以下)。

在 Vue 中,为什么在 watcher 的回调里不能立即获取到最新的 DOM?

因为 watcher 的回调与渲染 watcher 在同一个 tick 中执行,而 flushSchedulerQueue 会按 id 排序,确保渲染 watcher 最后执行。因此,在用户 watcher 的回调中,DOM 尚未更新,无法获取最新 DOM。

🏷️

标签

➡️

继续阅读