普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月7日首页

# Vue 渲染系统的四个关键阶段:从模板编译到新旧 VDOM Patch 的完整机制解析

作者 excel
2025年12月7日 07:55

现代 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.vnodeinstance.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 的真正内部机制。

❌
❌