刚接触 Vue 的时候,你可能会好奇:我明明刚改了数据,为什么 DOM 没有立刻变?Vue 为什么不直接同步更新?
现在从浏览器渲染原理出发,讲一下 Vue 做这个设计决策背后的理由。
1、DOM 操作为什么"贵"
要理解异步更新的价值,先得知道:操作 DOM 并不像改一个 JS 变量那么“便宜”。
每次你修改 DOM,浏览器都要做两件很重的事:
1.1 Reflow(重新布局)
元素的尺寸或位置变了,浏览器需要重新计算整个页面的布局:这个元素变大了,旁边的元素要不要移位?父容器要不要撑高?等等。
这是一个从上到下、牵一发动全身的计算过程,页面越复杂,越耗时。
1.2 Repaint(重新绘制)
布局算完了,浏览器把新的画面重新绘制到屏幕上。
即使你只改了一个小元素的颜色(不影响布局),也会触发 Repaint。
打个比方:DOM 操作就像建筑工地改图纸。改一处,不管改动大小,都得把整张图重新核对一遍、重新打印——这个过程是固定成本,改 1 次和改 10 次,每次的代价是一样的。
2、如果 Vue 选择同步更新,会发生什么
假设你在一个方法里连续修改了 3 个数据:
// 用户点击了「刷新」按钮
refresh() {
this.isLoading = true // 改数据 → 同步触发 DOM 更新 ①
this.list = [] // 改数据 → 同步触发 DOM 更新 ②
this.isLoading = false // 改数据 → 同步触发 DOM 更新 ③
}
同步更新意味着:
- 改
isLoading = true→ 浏览器重新布局+绘制一次 - 改
list = []→ 浏览器重新布局+绘制一次 - 改
isLoading = false→ 浏览器重新布局+绘制一次
共触发 3 次 Reflow + 3 次 Repaint。
但用户最终只能看到第 ③ 步之后的结果——前两次的渲染,对用户来说完全是浪费,用户甚至感知不到那两个中间状态(因为 JS 是单线程的,浏览器在 JS 执行期间不会渲染屏幕)。
3、Vue 的异步批处理策略
Vue 的实际做法是:
refresh() {
this.isLoading = true // 只记入队列,不动 DOM
this.list = [] // 只记入队列,不动 DOM
this.isLoading = false // 只记入队列,不动 DOM
}
// 当前 JS 执行完毕 → Vue 把队列里所有变化合并 → 只更新 DOM 一次
只触发 1 次 Reflow + 1 次 Repaint,结果和同步更新完全一样,但省掉了 2/3 的渲染开销。
这就是异步批处理的核心价值:把同一个 tick 内所有的数据变化合并,只做一次 DOM 更新。
4、真实场景下差多少
简单页面(改 2~3 个数据):差距不明显,现代浏览器处理得很快。
但在以下场景,差距会非常显著:
场景 1:大列表渲染
// 假设 list 有 100 条数据,每条数据单独更新某个字段
this.list.forEach(item => {
item.selected = false // 如果是同步更新,触发 100 次 DOM 更新
})
// Vue 异步批处理:100 次数据变化 → 合并 → 只更新 1 次 DOM
场景 2:用户高频交互
用户快速滑动进度条,每移动一下触发一次数据更新。
- 同步:每次滑动都重绘,60fps 下每秒 60 次 Repaint,页面容易卡顿
- 异步:同一帧内的多次更新合并成 1 次,流畅很多
场景 3:父子组件连锁更新
父组件数据变化导致 10 个子组件都需要更新:
- 同步:10 个子组件各自重绘一次 = 10 次 Repaint
- 异步:统一在一次 tick 内完成所有子组件更新 = 1 次 Repaint
在复杂应用中,异步批处理带来的性能提升可以是数倍到数十倍。
5、代价是什么?也就是:$nextTick 的由来
这个设计唯一的代价,就是:改完数据后,不能立刻读取 DOM。
this.showPanel = true
// ❌ 此刻 DOM 还没更新,panel 可能还不在页面上
const height = this.$refs.panel.offsetHeight // 拿到旧值或报错
解决方案就是 $nextTick,它让你在 Vue 完成这一轮 DOM 更新后,再执行需要读取 DOM 的逻辑:
this.showPanel = true
this.$nextTick(() => {
// ✅ DOM 已更新
const height = this.$refs.panel.offsetHeight // 正确
})
这是一个经典的用代码复杂度换运行性能的设计取舍——大多数情况下,你根本不需要在改完数据后立刻读 DOM,所以这个代价很小;而性能收益在复杂应用中非常可观。
6、总结
| 同步更新 | Vue 异步批处理 | |
|---|---|---|
| 每次数据变化 | 立刻触发 DOM 更新 | 记入队列,等 JS 执行完再统一更新 |
| 一个方法里改 N 次数据 | 触发 N 次 Reflow/Repaint | 只触发 1 次 |
| 代码复杂度 | 低(改完数据可立刻读 DOM) | 需要 $nextTick 协调 |
| 渲染性能 | 差(大量冗余渲染) | 好(只渲染最终状态) |
Vue 选择异步更新,本质上是在说:用户只关心最终结果,中间状态的渲染是浪费,我替你省掉它。