渲染机制与编译原理
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 节点树。
// 模板
<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-if、v-for、v-on等)并生成对应代码结构。 - 识别静态节点并标记(用于后续静态提升)。
- 处理插槽、作用域插槽。
- 计算 PatchFlag、动态属性列表等(Vue 3 编译优化核心)。
// 经过 transform 后,动态节点被标记
{
tag: 'div',
patchFlag: 1 /* TEXT */, // 表示该节点文本动态变化
dynamicProps: ['class']? // 若有动态属性会列出
}
第三步:代码生成(CodeGen)
根据转换后的 AST 生成 render 函数的代码字符串(或直接生成 function)。最终输出一个 render 函数,内部调用 h(createVNode)函数来构建虚拟节点。
// 生成的 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 对象。
// 编译前
<div>static text</div>;
// 编译后
const _hoisted_1 = /*#__PURE__*/ _createElementVNode("div", null, "static text", -1 /* HOISTED */);
function render() {
return _hoisted_1;
}
收益:减少重复创建 VNode 的内存和时间开销。
核心优化二:PatchFlag 精准标记动态节点类型
在编译时为每个节点标记一个 patchFlag 位掩码,告诉运行时在更新时只需要检查哪些类型的变更。
| PatchFlag 值 | 含义 | 描述 |
|---|---|---|
| 1 | TEXT | 文本内容动态变化 |
| 2 | CLASS | class 绑定动态变化 |
| 4 | STYLE | style 绑定动态变化 |
| 8 | PROPS | 其他属性动态变化 |
| 16 | FULL_PROPS | 需要完整 diff props(动态 key) |
| 32 | HYDRATE_EVENTS | 事件监听器需要 hydrating(SSR 专用) |
| 64 | STABLE_FRAGMENT | 子节点顺序稳定(可简化 diff) |
| 128 | KEYED_FRAGMENT | 带 key 的 fragment |
| 256 | UNKEYED_FRAGMENT | 不带 key 的 fragment |
Diff 时,根据 patchFlag 跳过不必要的遍历:
if (patchFlag & PatchFlags.TEXT) {
// 只对比文本内容
} else if (patchFlag & PatchFlags.PROPS) {
// 只对比 props 中指定的动态属性
}
核心优化三:块树(Block Tree)结构
问题:传统虚拟 DOM 的 Diff 需要递归遍历整个树,即使很多节点是静态的。
解决:Vue 3 引入 Block 概念。编译器将带有结构指令(v-if、v-for、v-for 内部的动态节点等)的节点收集到一个扁平数组中。每个 Block 维护一个 dynamicChildren 列表,仅包含动态节点。
// 渲染时
_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)
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 值 | 作用 | 常见场景 |
|---|---|---|---|
| 元素 VNode | string(如 'div') | 表示真实 DOM 元素 | <div>、<span> 等 |
| 组件 VNode | 组件对象(Component) | 表示用户定义的组件 | <MyComponent> |
| 文本 VNode | Symbol(Text) | 纯文本节点 | 插值、文本内容 |
| 注释 VNode | Symbol(Comment) | 注释节点 | 很少直接接触 |
| 静态 VNode | string + 特殊标记 | 静态提升节点 | 被提升的静态元素 |
| 插槽 VNode | Symbol(Fragment) + 特殊处理 | 表示插槽内容 | <slot> 生成 |
| Fragment | Symbol(Fragment) | 多根节点或 v-for 产生的片段 | 模板片段 |
VNode 核心属性(简化)
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_COMPONENT、SUSPENSE、TELEPORT)。
总结
| 主题 | 关键点 |
|---|---|
| 模板编译 | 解析 → 转换 → 生成 render;产出带有优化标记的代码 |
| Diff 优化 | 静态提升(复用 VNode)、PatchFlag(精准更新)、Block Tree(扁平动态节点) |
| 编译时 vs 运行时 | Vue 3 强调编译时标记,减少运行时比对,性能优于 Vue 2 |
| 自定义渲染器 | 通过 createRenderer 实现跨平台,需要平台特定的节点操作 API |
| VNode 类型 | 元素、组件、文本、Fragment、插槽等;通过 shapeFlag 快速区分并处理 |
理解这些底层机制,不仅能写出更优雅的代码(例如利用静态提升避免不必要的计算),也能在需要深度定制渲染行为时游刃有余。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Vue:渲染机制 | 官方深度说明 |
| Vue:渲染函数 | render / h |
| @vue/compiler-core | 编译器源码(英文) |
相关文章
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
状态管理:Pinia 与 Vuex
Pinia 是 Vue 官方推荐的状态管理库(Vue 3 新项目默认选型)。Vuex 4 仍可与 Vue 3 配合,用于维护旧项目。见 Vue 基础。
Vue 生态关键库深度集成
VueUse、Vite 与 Nuxt 是 Vue 3 生态中常用的效率与工程化方案。概览见 Vue 基础。
依赖收集与派发更新机制
说明 Vue 3 中 track / trigger 与 targetMap 的工作方式。前置:v2-v3 差异。
测试策略
Vue 应用常用 Vitest + @vue/test-utils 做单元/组件测试,Playwright 或 Cypress 做 E2E。见 Vue 基础。
组合式 API 与逻辑复用设计模式
组合式 API 与 Composables 是 Vue 3 推荐的逻辑组织方式。概览见 Vue 基础。