Vue3的渲染核心是响应式系统 和 虚拟 DOM (Virtual DOM) 。它的渲染过程可以概括为以下几个关键阶段:挂载 (Mount) 、打补丁 (Patch) 和 更新 (Update) 。
1. 响应式系统(Reactivity System)
在渲染流程开始之前,必须理解 Vue 3 如何知道数据发生了变化。
-
核心机制:Proxy & Track/Trigger
- Vue 3 使用 ES6 Proxy 来劫持数据的读写操作,替换了 Vue 2 的
Object.defineProperty。 - 当组件 首次渲染 时,会读取响应式数据,Proxy 会追踪 (Track) 依赖,记录下“哪个组件的哪个 effect (副作用,即渲染函数)”依赖了这个数据。
- 当响应式数据被 修改 时,Proxy 会拦截到写入操作,并触发 (Trigger) 之前记录的所有依赖(即运行相关的 effect 函数)。
- Vue 3 使用 ES6 Proxy 来劫持数据的读写操作,替换了 Vue 2 的
-
与渲染的关系: 当数据变化触发 effect 时,这个 effect 就会调度执行组件的更新函数 (render function) ,从而进入后续的 VDOM 渲染和 Diff 流程。
2. 虚拟 DOM (Virtual DOM - VDOM)
渲染的中间层,是一个轻量级的 JavaScript 对象树,它描述了真实 DOM 树的结构。
-
VNode (Virtual Node): VDOM 树的每个节点都是一个 VNode 对象。
-
它包含了描述真实 DOM 元素所需的一切信息,例如:
type: 元素的类型 (如'div','p') 或组件定义。props: 元素的属性或组件的 props。children: 子 VNode 数组。key: 用于 Diff 算法的唯一标识。el: 对应的真实 DOM 节点 (在挂载后)。
-
-
作用: 将复杂的 DOM 操作抽象化,让 Diff 算法可以直接在 JS 对象上进行比较,最大程度地减少直接操作真实 DOM 的次数,因为 DOM 操作的性能开销最大。
3. 渲染流程:挂载 (Mount) 与 打补丁 (Patch)
Vue 3 的渲染流程可以分为 首次渲染 (Mount) 和 后续更新 (Patch) 两种情况。
🔹 首次渲染(Mounting)
- 执行 Setup/Render 函数: 运行组件的
setup函数,并执行编译后的 渲染函数 (render function) 。 - 生成 VDOM 树 (VNode Tree): 渲染函数返回一个完整的 新的 VDOM 树 (New VNode Tree) 。
- VNode 真实 DOM: 框架调用
mount过程,遍历新的 VNode 树,递归地创建对应的真实 DOM 节点 (document.createElement)。 - 关联并插入: 将真实 DOM 节点存储到 VNode 的
el属性上,并将整个 DOM 结构插入到容器元素中。
🔹 后续更新(Patching)
当响应式数据变化时,会触发组件的更新函数,进入 Patch (打补丁) 流程。
- 生成新的 VDOM 树 (New VNode Tree): 再次执行渲染函数,生成一个代表当前状态的 新的 VDOM 树。
- 获取旧 VDOM 树 (Old VNode Tree): 使用上一次渲染生成的 VDOM 树作为旧树。
- Diff 算法比较: 框架调用
patch过程,使用 Diff 算法 对 新 VNode 和 旧 VNode 进行逐层递归比较。 - 最小化 DOM 操作: Diff 算法会找出两棵树的差异 (即“补丁”)。
- 应用补丁: 框架根据这些差异,只对真实 DOM 中需要修改的部分进行精确的操作(如更新文本、修改属性、移动/删除/创建元素),而不是重新创建整个 DOM。
🌟 性能优化:静态提升 (Static Hoisting) 与 Block Tree
Vue 3 在编译阶段做了大量的优化,生成 VNode 时会跳过那些不会变化的静态内容(静态提升),并且只追踪和比较 VNode 树中动态绑定的部分(Block Tree)。这使得 Vue 3 的 Diff 效率远高于纯粹的 VDOM 比较。
🖼️ Vue 3 渲染流程图
以下是 Vue 3 从数据变化到最终屏幕更新的完整流程图:
graph TD
A[开始: createApp] --> B[创建应用实例]
B --> C[mount挂载]
C --> D[创建根组件实例]
D --> E[初始化组件]
E --> E1[处理 props]
E --> E2[处理 slots]
E --> E3[执行 setup 函数]
E3 --> F[创建响应式数据]
F --> F1[reactive/ref API]
F1 --> F2[Proxy 代理对象]
E --> G[编译模板]
G --> G1{是否有模板?}
G1 -->|有| G2[模板编译为 render 函数]
G1 -->|无| G3[使用 render 函数]
G2 --> H[执行 render 函数]
G3 --> H
H --> I[依赖收集阶段]
I --> I1[访问响应式数据]
I1 --> I2[触发 getter]
I2 --> I3[收集副作用函数 effect]
H --> J[生成虚拟 DOM VNode]
J --> J1[VNode 树结构]
J1 --> J2[包含静态标记 PatchFlag]
J --> K{首次渲染?}
K -->|是| L[挂载 mount]
K -->|否| M[更新 patch]
L --> L1[创建真实 DOM]
L1 --> L2[处理子节点]
L2 --> L3[插入到容器中]
L3 --> L4[执行 mounted 钩子]
L4 --> N[渲染完成]
M --> M1[Diff 算法比较]
M1 --> M2[比较 VNode 类型]
M2 --> M3{节点类型相同?}
M3 -->|是| M4[复用节点, 更新属性]
M3 -->|否| M5[替换节点]
M4 --> M6[对比子节点]
M6 --> M7[使用 key 优化]
M7 --> M8[最长递增子序列算法]
M8 --> M9[最小化 DOM 操作]
M5 --> M9
M9 --> M10[执行 updated 钩子]
M10 --> N
N --> O{数据是否变化?}
O -->|是| P[触发 setter]
O -->|否| Q[等待交互]
P --> P1[通知依赖的 effect]
P1 --> P2[将组件加入更新队列]
P2 --> P3[异步批量更新 nextTick]
P3 --> H
Q --> R{用户交互?}
R -->|是| S[事件处理]
R -->|否| Q
S --> T[更新数据]
T --> P
style A fill:#e1f5e1
style N fill:#e1f5e1
style F2 fill:#fff4e1
style J1 fill:#fff4e1
style M8 fill:#e1f0ff
style P3 fill:#ffe1e1