# Vue 渲染系统的四个关键阶段:从模板编译到新旧 VDOM Patch 的完整机制解析
现代 UI 框架的核心目标是在数据变化时以最小代价更新视图。Vue 通过将整个渲染流程拆分为四个阶段,使得 UI 架构具备可操作性、可维护性与高性能。本文将系统介绍 Vue 中:
- 模板如何生成渲染逻辑
- 组件如何组织渲染过程
- 响应式系统如何触发更新
- 新旧 VDOM 如何对比和更新 DOM
你将清楚理解 渲染函数来自哪里,渲染 effect 如何被触发,以及新旧 VDOM 在整个生命周期中如何被获取和使用。
第一阶段:模板编译(Template Compilation)——从静态模板到可执行渲染逻辑
所有 Vue 组件都从 template 开始,但模板本质上只是字符串,Vue 无法直接执行,因此编译阶段负责将模板转化为最终的渲染函数(render)。
以一个简单模板为例:
<div>{{ count }}</div>
编译器会将其解析为 AST(抽象语法树),再转换为具有结构化意义的渲染代码,最终生成的渲染函数类似:
function render(_ctx) {
return createElementBlock("div", null, _ctx.count, 1 /* TEXT */)
}
在这一阶段,有三项关键优化能力:
1. 静态与动态节点区分
静态节点被提升,只生成一次;动态节点会带有 PatchFlag,如 TEXT、PROPS 等,用于精确更新。
2. 渲染函数是纯 JS,不包含 DOM 操作
渲染函数只会构建 VNode(虚拟 DOM),不会直接访问浏览器 DOM。
3. 结构化信息为后续的 diff 提供优势
编译后的代码比运行时解释模板更快,可直接参与 diff 计算。
编译阶段的产物 render() 是 UI 渲染的核心入口,也是所有 VDOM 的唯一来源。
第二阶段:组件渲染 effect(Render Effect)——渲染函数的执行与 VDOM 的生成者
每个 Vue 组件实例在初次挂载时都会创建一个 渲染 effect。这是 Vue 中真正负责“渲染 UI”与“响应式驱动更新”的核心单元。
Vue 内部会执行:
instance.update = effect(() => {
const subTree = render(instance.ctx)
patch(instance.vnode, subTree)
instance.vnode = subTree
})
这个渲染 effect 具备以下职责:
1. 执行渲染函数并生成新的 VNode(新 VDOM)
渲染函数访问 ctx 中的响应式数据,从而生成一套全新的虚拟 DOM 树。
2. patch 负责向真实 DOM 提交变化
渲染 effect 本身不处理 DOM,它只是负责调用 patch 让 DOM 更新。
3. effect 会在执行过程中记录依赖
访问 _ctx.xxx 时,响应式 getter 会执行 track,将渲染 effect 与数据绑定。
4. 渲染 effect 是组件级而非节点级
无论模板多大,组件始终只有一个渲染 effect。
渲染 effect 是“新 VDOM 的来源者”,并同时保存上一次渲染的旧 VDOM。
这一点非常关键,下一阶段我们将看到如何利用它。
第三阶段:响应式触发(Reactive Trigger)——数据变化驱动重新渲染的机制核心
当响应式数据修改时:
state.count++
Vue 的响应式系统会执行:
1. setter → trigger(target, key)
找到依赖该 key 的所有 effect,此处即 渲染 effect。
2. 调度渲染 effect 进入队列(scheduler)
Vue 不会立即重新渲染,而是进行批处理,以优化性能。
3. 下一轮事件循环执行渲染 effect
重新执行:
render(ctx)
patch(oldVNode, newVNode)
这是一次完整的 UI 更新动作:
- 获取新 VDOM(新 vnode)
- 与旧 VDOM(上次 vnode)进行 diff
- 将真实 DOM 局部更新到最新状态
响应式系统的核心作用不是更新 DOM,而是触发渲染 effect。
DOM 更新发生在下一阶段。
第四阶段:虚拟 DOM Diff 与 DOM Patch —— 新旧 VDOM 的来源与交替
这部分是你最关心的:“新旧 VDOM 分别从哪里来?Vue 是如何获取它们并做 diff 的?”
我们先给出答案:
新 VDOM 的来源:由当前执行的 render() 生成
在渲染 effect 内:
const newVNode = render(instance.ctx)
每次执行 render,都会返回一棵全新的 VNode 树,这就是 新 VDOM。
它不来自缓存、不来自 DOM,而是 render 的直接产物。
旧 VDOM 的来源:组件实例 instance.vnode 中保存的上一次结果
第一次渲染时,Vue 会执行:
instance.vnode = render(instance.ctx)
patch(null, instance.vnode)
第二次更新时:
const newVNode = render(ctx)
patch(instance.vnode, newVNode)
instance.vnode = newVNode
可以总结:
-
旧 VDOM = 上一次渲染函数生成的 VNode,被 Vue 存储在
instance.vnode或instance.subTree中 - 新 VDOM = 当前渲染函数生成的 VNode
它们都来自渲染函数,不通过 DOM 查询获得。
完整的生命周期结构如下:
第一次:
newVNode = render()
patch(null, newVNode)
oldVNode ← newVNode
第二次:
newVNode = render()
patch(oldVNode, newVNode)
oldVNode ← newVNode
第三次:
newVNode = render()
patch(oldVNode, newVNode)
oldVNode ← newVNode
它们在每次更新中不断交替,像接力棒一样传递。
总结:Vue 渲染系统的完整四阶段链路
最终我们可以将 Vue 的渲染管线抽象为四个连续的大阶段:
1) Template Compile
模板 → 渲染函数(纯 JS、无 DOM)
2) Render Effect
执行渲染函数 → 得到新 VDOM
保存旧 VDOM → instance.vnode
3) Reactivity Trigger
数据变化 → 触发渲染 effect 重新执行
4) Patch (Diff → DOM Update)
patch(oldVNode, newVNode)
最小成本更新真实 DOM
其中:
- 新 VDOM 来自 render()
- 旧 VDOM 来自组件实例保存的上一次 render() 结果
- patch 是最终落实 DOM 更新的唯一阶段
这就是 Vue 数据变化后自动更新 UI 的真正内部机制。