批量更新是响应式系统将多次数据变更合并为一次执行的机制,通过异步微任务队列实现延迟执行与任务去重,确保同一组件仅渲染一次、状态取最终值,并在浏览器渲染前完成DOM更新。

批量更新是指响应式系统在一次事件循环中,将多个数据变更引发的更新任务合并为一次执行,避免重复渲染或计算。它不是“等所有变化做完再更新”,而是通过异步队列 + 去重机制,在 JS 同步代码执行完毕后、浏览器渲染前,统一刷新所有待处理的副作用。
为什么多次赋值只触发一次视图更新?
关键在于“延迟执行”和“任务去重”。当 state.count++ 执行 10 次时,每次修改都会触发 trigger,但不会立刻运行组件的 render 函数;而是调用 queueJob 把渲染任务加入全局队列。该函数会检查任务是否已在队列中,相同任务(比如同一个组件的更新)只保留一份。
- 同一组件的渲染任务,无论被加入队列多少次,最终只执行一次
- 不同组件或 watch/computed 的更新任务,只要发生在同一 tick 内,也会被收进同一个队列
- 队列不会马上执行,而是通过 Promise.resolve().then(flushJobs) 推入微任务队列
更新时机:为什么是微任务而不是 setTimeout?
Vue 默认使用微任务(如 Promise.then 或 queueMicrotask)来调度 flushJobs,因为微任务在当前 JS 栈清空后、渲染前立即执行,延迟最小。这保证了:
- 用户操作(如点击、输入)后的状态更新能尽快反映到界面上,不卡顿
- 多个同步修改共享最新状态——比如连续设置 a = 1; a = 2; a = 3,render 函数拿到的是最终值 3,而非中间态
- 若降级为 setTimeout(宏任务),则要等到下一轮事件循环,UI 响应变慢
底层依赖哪些核心变量和逻辑?
批量更新不是黑盒,它由几个关键结构协同完成:
- queue:存放待执行的 job(effect.run、component update 等)
- isFlushPending:布尔标记,确保微任务只被调度一次,防止重复注册 Promise.then
- activeEffect / targetMap:支撑依赖收集,让 trigger 知道“谁该被加入 queue”
- effect.scheduler:每个副作用可自定义调度方式,runtime-core 中统一设为 queueJob
它和 nextTick 是什么关系?
nextTick 不是批量更新的原因,而是它的出口和接口。flushJobs 执行完后,所有 DOM 更新已完成,此时调用 nextTick 回调,开发者就能安全地读取更新后的 DOM 元素。换句话说:
- 你写的 this.count++ → 触发 queueJob → 注册微任务 → flushJobs → 更新 DOM
- 你写的 nextTick(() => {...}) → 被推入另一个回调队列 → 在 flushJobs 完成后立即执行
- 所以 nextTick 是“等批量更新结束”,不是“等某个数据更新”

















