人人都会AI编程

6.4 组件更新渲染全流程:数据变更 → 异步更新队列 → Diff 对比 → 局部 DOM 更新

更新时间:2026-07-09

Vue 的视图更新并非数据一改变就立刻操作 DOM,而是经历一套精心设计的流程。了解这个流程能帮你理解“为什么有时数据变了界面没变”“为什么 nextTick 能拿到更新后的 DOM”等实际问题。

整个更新流程可以分解为四个关键阶段:

第一阶段:数据变更(触发响应式通知)

当我们修改一个响应式数据,比如 count.value++,Proxy 拦截到这次修改后,并不会直接去修改 DOM。它做的事情是 通知依赖这个数据的所有副作用(effect)

在 Vue 3 中,每个组件的渲染函数都被包裹在一个副作用函数里。副作用会在创建时收集自己依赖了哪些响应式数据,当这些数据变化时,响应式系统(通过 trigger)找到对应的副作用,把它们标记为“需要重新执行”。

但注意:这里只是标记,没有立即执行。

第二阶段:异步更新队列(批量合并,避免重复更新)

如果每修改一个数据就立即执行一遍渲染函数,那下面这样的代码就会出错:

count.value++
count.value++
count.value++

三次修改会触发三次渲染,但前两次渲染的结果马上又被覆盖,产生大量无意义的 DOM 操作。为了解决这个问题,Vue 实现了一套 异步更新队列 机制。

核心策略:

  • 当一个组件的副作用被标记为“脏”时,它不会立即执行,而是把自己放进一个 微任务队列(Promise.then) 里。
  • 同一个副作用在同一个“同步任务”中被多次标记,最终只会入队一次(去重)。
  • 当浏览器完成当前的同步任务,准备处理微任务时,Vue 才会统一执行这些等待更新的副作用。

也就是说,上面的三次 count.value++ 虽然修改了三次数据,但对应的渲染副作用只会在最后执行一次,拿到的是最终的值。这极大地减少了重渲染次数,也避免了中间状态被展示出来。

nextTick 的作用:
这个异步队列对外暴露了 nextTick 接口。当我们想在“DOM 真正更新完成”后做点事(比如获取某个元素的尺寸),就需要用:

count.value++
await nextTick()
// 此时 DOM 已经更新,可以安全读取
console.log(document.getElementById('myDiv').textContent)

nextTick 内部会把回调压入同一个微任务队列的更后面,保证它一定在更新副作用执行完后再触发。

第三阶段:Diff 对比(虚拟 DOM 树的最小化变更)

当副作用(组件的渲染函数)被重新执行时,它会生成一棵新的虚拟 DOM 树。Vue 将新旧虚拟 DOM 树进行同层对比(Diff),找出哪些节点需要新增、删除、更新属性或移动位置。

Vue 3 的 Diff 并非全量“暴力遍历”,它结合了编译阶段给出的优化标记:

  1. 静态提升与 PatchFlags

编译器在编译模板时,会识别哪些节点是绝对静态的(永远不会变),并“提升”到渲染函数之外。运行时 Diff 直接跳过这些静态节点,连对比都不用。
对于动态节点,编译器会打上 PatchFlags(1 表示文本变化,2 表示 class 变化……)。Diff 时只看这些标记,只检查对应属性有没有变化,避免全量比对。

  1. 采用同层比较

如果父节点类型变了(比如从 <div> 变成 <p>),那么它和它的所有子节点都不会再做精细对比,而是直接“整块替换”。因为大概率子节点也都变了,做细节对比不划算。

  1. 列表 Diff 与最长递增子序列

对于 v-for 列表,Vue 3 会根据节点的 key 进行 端对比 + 最长递增子序列 算法。它会先通过头部、尾部移动减少需要移动的节点数量,再计算最小移动开销,仅移动那些真正需要换位置的节点,大幅减少 DOM 操作。

通过这套策略,Diff 过程最终产出一份 patch 补丁,即一组最精简的 DOM 操作指令(添加、删除、移动、更新属性、更新文本等)。

第四阶段:局部 DOM 更新

Diff 生成的 patch 指令会被真正应用到浏览器的真实 DOM 上。这一步就是 将计算出的最小变更集合,一次性提交给 DOM

因为更新过程是批量的,且只操作那些真正发生了变化的部分,所以用户最终会看到界面精准、高效地变成新状态。你不会看到中间过程的闪烁,也不会出现“整个界面重新渲染一遍”的低效情况。

总结这条链路:

数据修改 
→ 响应式系统通知组件依赖 
→ 组件副作用被标记为“脏”并加入异步队列(去重)
→ 微任务执行时,重新运行渲染函数生成新虚拟 DOM
→ 新旧虚拟 DOM Diff 产出最小补丁
→ 将补丁应用到真实 DOM,完成局部更新

掌握这条链路后,很多行为就不再神秘:

  • 为什么 console.log(el.textContent) 拿到旧值?因为 DOM 还没更新,你得用 nextTick 等异步队列执行完。
  • 为什么大量数据同时修改,界面依然丝滑?因为异步队列把多次更新合并了。
  • 为什么一个复杂列表,只改了一个对象属性,DOM 操作却很少?因为 Diff 只变了一个 li 里的文本,其他 li 都是复用的。

理解这个流程,是把 Vue 从“会用”提升到“能排查问题、能针对性优化”的关键一步。