FORMA

渲染机制与编译原理

Vue 的渲染由编译时(模板 → render)与运行时(VNode → DOM)组成。见 Vue 基础

Vue 的渲染系统由编译时(模板 → render 函数)和运行时(虚拟 DOM → 真实 DOM)两部分组成。理解其内部机制,有助于写出高性能代码,甚至扩展 Vue 到不同平台。

1. 模板编译三部曲:Template Parser → AST 转换 → CodeGen 生成 Render 函数

Vue 单文件组件(.vue)中的 <template> 会被编译成 render 函数,整个过程分为三个阶段,出自 @vue/compiler-dom@vue/compiler-core

第一步:模板解析(Parser)

将模板字符串转换为抽象语法树(AST)。解析器使用有限状态机(Finite State Machine)逐个字符读取模板,识别开始标签、结束标签、文本、插值表达式、指令等,生成 AST 节点树。

js
// 模板
<div class="container">Hello {{ name }}</div>

// 简化后的 AST 节点
{
  type: 1,  // NodeTypes.ELEMENT
  tag: 'div',
  props: [{ name: 'class', value: { content: 'container' } }],
  children: [
    { type: 2, content: 'Hello ' },   // NodeTypes.TEXT
    { type: 5, content: { content: 'name' } } // NodeTypes.INTERPOLATION
  ]
}

第二步:AST 转换(Transform)

遍历 AST,进行一系列优化和转换:

  • 解析指令(v-ifv-forv-on 等)并生成对应代码结构。
  • 识别静态节点并标记(用于后续静态提升)。
  • 处理插槽、作用域插槽。
  • 计算 PatchFlag、动态属性列表等(Vue 3 编译优化核心)。
js
// 经过 transform 后,动态节点被标记
{
  tag: 'div',
  patchFlag: 1 /* TEXT */,   // 表示该节点文本动态变化
  dynamicProps: ['class']?   // 若有动态属性会列出
}

第三步:代码生成(CodeGen)

根据转换后的 AST 生成 render 函数的代码字符串(或直接生成 function)。最终输出一个 render 函数,内部调用 h(createVNode)函数来构建虚拟节点。

js
// 生成的 render 函数(简化)
function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock("div", { class: "container" }, ["Hello " + _toDisplayString(_ctx.name)])
  );
}

对于 Vue 3,编译结果通常包含 _createElementBlock_createElementVNode_openBlock 等辅助函数。

2. 虚拟 DOM 的 Diff 算法(Vue 3 优化版)

Vue 3 在保留虚拟 DOM 灵活性的前提下,通过编译时信息大幅提升了 patch 过程的效率。

核心优化一:静态提升(Static Hoisting)

编译时会将纯静态的节点(不依赖任何动态数据)提升到渲染函数外部,使得它们只在应用启动时创建一次,每次渲染复用同一个 VNode 对象。

js
// 编译前
<div>static text</div>;

// 编译后
const _hoisted_1 = /*#__PURE__*/ _createElementVNode("div", null, "static text", -1 /* HOISTED */);

function render() {
  return _hoisted_1;
}

收益:减少重复创建 VNode 的内存和时间开销。

核心优化二:PatchFlag 精准标记动态节点类型

在编译时为每个节点标记一个 patchFlag 位掩码,告诉运行时在更新时只需要检查哪些类型的变更。

PatchFlag 值含义描述
1TEXT文本内容动态变化
2CLASSclass 绑定动态变化
4STYLEstyle 绑定动态变化
8PROPS其他属性动态变化
16FULL_PROPS需要完整 diff props(动态 key)
32HYDRATE_EVENTS事件监听器需要 hydrating(SSR 专用)
64STABLE_FRAGMENT子节点顺序稳定(可简化 diff)
128KEYED_FRAGMENT带 key 的 fragment
256UNKEYED_FRAGMENT不带 key 的 fragment

Diff 时,根据 patchFlag 跳过不必要的遍历:

js
if (patchFlag & PatchFlags.TEXT) {
  // 只对比文本内容
} else if (patchFlag & PatchFlags.PROPS) {
  // 只对比 props 中指定的动态属性
}

核心优化三:块树(Block Tree)结构

问题:传统虚拟 DOM 的 Diff 需要递归遍历整个树,即使很多节点是静态的。

解决:Vue 3 引入 Block 概念。编译器将带有结构指令(v-ifv-forv-for 内部的动态节点等)的节点收集到一个扁平数组中。每个 Block 维护一个 dynamicChildren 列表,仅包含动态节点。

js
// 渲染时
_openBlock();   // 开启 Block
_createElementBlock("div", ...); // 创建 Block 根节点
// 内部动态节点都会被收集到 Block 的 dynamicChildren 中

更新时,运行时只遍历 dynamicChildren 列表,而不会递归到静态子树。这使 Diff 复杂度从 O(n³) 降低到与动态节点数量线性相关

3. 编译时优化 vs 运行时优化

维度编译时优化(Vue 3 主导)运行时优化(Vue 2 为主)
静态提升✅ 移动到 render 外部❌ 每次 render 重新创建
PatchFlag✅ 编译时收集动态信息❌ 需要运行时比对
Block Tree✅ 收集动态节点路径❌ 全量递归 Diff
缓存事件处理器✅ 自动缓存内联函数❌ 需手动或借助 .once
v-once编译时标记静态内容运行时缓存节点

结论:Vue 3 将大量优化工作前置到编译阶段,减少了运行时的工作量,从而提升性能。

4. 自定义渲染器:createRenderer 实现跨平台渲染

Vue 3 将核心渲染逻辑与平台相关操作分离,通过 createRenderer 函数可以构建自定义渲染器,用于非浏览器环境(如小程序、Canvas、WebGL、原生移动端)。

核心原理

scheduler 和渲染器需要实现一套节点操作 API(NodeOp),例如:

  • createElement(type)
  • insert(child, parent, anchor)
  • setElementText(node, text)
  • patchProp(el, key, prevValue, nextValue)
js
import { createRenderer } from '@vue/runtime-core';

const { render, createApp } = createRenderer({
  // 平台特定的节点操作
  createElement(type) { ... },
  patchProp(el, key, prev, next) { ... },
  insert(child, parent, anchor) { ... },
  // ... 其他必须方法
});

// 然后可以用 createApp 创建跨平台应用
const app = createApp(RootComponent);
app.mount(platformContainer);

已有实践

  • Weex:基于 Vue 2 的跨平台移动端方案,Vue 3 版本使用自定义渲染器。
  • mpvue / uni-app:将 Vue 组件编译为小程序代码,其核心也是自定义渲染器。
  • vue-threejs:将 Vue 组件渲染到 Three.js 场景。

5. VNode 的结构与类型

VNode(虚拟节点)是 Vue 运行时最小的表示单元,类型不同其结构和处理方式也有所差异。

VNode 类型type作用常见场景
元素 VNodestring(如 'div'表示真实 DOM 元素<div><span>
组件 VNode组件对象(Component表示用户定义的组件<MyComponent>
文本 VNodeSymbol(Text)纯文本节点插值、文本内容
注释 VNodeSymbol(Comment)注释节点很少直接接触
静态 VNodestring + 特殊标记静态提升节点被提升的静态元素
插槽 VNodeSymbol(Fragment) + 特殊处理表示插槽内容<slot> 生成
FragmentSymbol(Fragment)多根节点或 v-for 产生的片段模板片段

VNode 核心属性(简化)

js
interface VNode {
  type: any;           // 元素标签名或组件对象或 Symbol
  props: any;          // 属性对象
  children: any;       // 子节点(数组或字符串)
  el: Node | null;     // 挂载的真实 DOM 节点
  key: string | number | null;   // 用于 diff 的 key
  component: ComponentInternalInstance | null; // 组件实例(若为组件 VNode)
  patchFlag: number;   // 优化标记
  shapeFlag: number;   // 节点形状(元素、组件、文本等)
  // ...
}

为什么 VNode 需要 shapeFlag:因为 type 可能是字符串或对象,运行时需要快速知道节点类型以选择处理逻辑。shapeFlag 使用位掩码高效组合多种特征(如 STATEFUL_COMPONENTSUSPENSETELEPORT)。

总结

主题关键点
模板编译解析 → 转换 → 生成 render;产出带有优化标记的代码
Diff 优化静态提升(复用 VNode)、PatchFlag(精准更新)、Block Tree(扁平动态节点)
编译时 vs 运行时Vue 3 强调编译时标记,减少运行时比对,性能优于 Vue 2
自定义渲染器通过 createRenderer 实现跨平台,需要平台特定的节点操作 API
VNode 类型元素、组件、文本、Fragment、插槽等;通过 shapeFlag 快速区分并处理

理解这些底层机制,不仅能写出更优雅的代码(例如利用静态提升避免不必要的计算),也能在需要深度定制渲染行为时游刃有余。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vue:渲染机制官方深度说明
Vue:渲染函数render / h
@vue/compiler-core编译器源码(英文)

Series

vue

9 / 16