Vue 渲染与更新流程
从状态变更到 DOM 更新的主链路:依赖收集、触发、调度、虚拟 DOM patch。
#synthesis
#growing
#frame
#vue3
[!info] Related notes
Vue 渲染与更新流程
这个概念为什么出现
只知道 ref 一改页面会变,却说不清:
- 为什么多次修改只渲染一次
nextTick为何存在- 组件更新边界在哪
需要一条从响应式触发到 DOM patch 的主链路。
[!abstract] 一句话理解 状态变更 → 触发依赖 → 调度组件更新任务 → 重新渲染 VNode → patch 到 DOM。
最小可观察示例
场景:一次点击内批量记录三次扫码
<script setup>
import { ref, nextTick } from 'vue'
const qty = ref(0)
const button = ref(null)
async function scanBurst() {
qty.value++
qty.value++
qty.value++
// 状态已是新值,当前这段同步代码中的 DOM 仍是本轮更新前的值。
console.log('before tick', button.value?.textContent, qty.value)
await nextTick()
console.log('after tick', button.value?.textContent, qty.value)
}
</script>
<template>
<button ref="button" type="button" @click="scanBurst">{{ qty }}</button>
</template>
在已挂载的 Vue 3 单文件组件中第一次点击,预期日志为 before tick 0 3、after tick 3 3。三次赋值发生在同一段同步代码内;三次独立用户点击不保证合并为一次更新。
核心链路
flowchart LR
A[改 ref/reactive] --> B[trigger 依赖]
B --> C[scheduler 排队]
C --> D[组件 render 出 VNode]
D --> E[diff/patch DOM]
- 依赖收集:渲染时 track 用到的响应式数据
- 触发:响应式值发生有效变更时通知依赖;相同值赋回不一定触发更新,未被当前渲染使用的状态也不必引起该组件更新
- 调度:组件更新任务默认排队并去重;状态值自身已同步改变,不是等 nextTick 才变
- 渲染:生成新 VNode 树
- 提交:diff/patch 更新需要变化的 DOM,结合编译期优化;不是承诺全局最少的 DOM 操作数
边界
- 详情以当前 Vue3 实现为准,名称是教学模型
- watch/watchEffect 有自己的调度选项;例如 flush: sync 会同步执行,不能把组件批处理规则套到所有 effect
- nextTick 等待当前待处理的 Vue DOM 更新,不保证浏览器已经绘制,也不等待网络请求、图片、字体或动画完成
- 本文描述 Vue 3 的常规 Virtual DOM 组件渲染;不同版本及渲染模式的内部实现需分别核对
- 直接操作 DOM 可能被后续 patch 覆盖
工程实践
- 需要 DOM 测量时用
nextTick - 大列表优化先看更新粒度与 key
- 避免在渲染路径制造新的不必要响应式对象
验证记录:Vue 3.5.43 + jsdom 30.1.2 下编译并挂载原单文件组件,首次点击日志为 before tick 0 3、after tick 3 3;另验证同段三次修改触发同步 watcher 3 次、默认 watcher 1 次。未将 DOM 更新视为屏幕绘制验证。
本节总结
Vue 更新是“响应式通知 + 调度合并 + VDOM 提交”的流水线,不是每个赋值立即同步改 DOM。
自测题
- 为什么多次
qty.value++通常不会同步渲染三次? nextTick解决什么?
参考答案
- 更新调度会合并到同一轮渲染。
- 在相关状态修改后等待 Vue 的 DOM 更新完成,再读取 DOM;它不是浏览器绘制完成或全部异步任务完成的信号。