虚拟 DOM 与编译优化
约 1115 字大约 4 分钟
布欧-Lewyon
2026-05-15
首页 › Vue › 工程化与架构 › 虚拟 DOM 与编译优化
Vue 在运行时跟踪响应式数据的变化,但更关键的优化发生在编译时——Vue 的模板编译器在构建阶段对模板做静态分析,大大减少了运行时的 Diff 开销。
VNode 结构
// 简化版
interface VNode {
__v_isVNode: true
type: string | Component | Fragment | Text | Comment | Static
props: Record<string, any> | null
children: VNode[] | string | null
shapeFlag: number // 节点类型标记(元素/组件/文本/Fragment)
patchFlag: number // ⭐ 编译优化标记
dynamicProps: string[] | null // 动态属性列表
dynamicChildren: VNode[] | null // Block Tree 优化
key: string | number | null
}模板编译流程
// 模板:<div id="app">{{ count }}</div>
// 编译后:
function render(_ctx, _cache) {
return _openBlock(), _createBlock('div', { id: 'app' }, [
_createTextVNode(_toDisplayString(_ctx.count)), // 只有这里需要更新
], 8 /* TEXT */)
}patchFlag
编译时分析出的优化标记,直接告诉运行时"这里只需要更新什么":
// packages/shared/src/patchFlags.ts
export const enum PatchFlags {
TEXT = 1, // 只有文本变化({{ msg }})
CLASS = 2, // 只有 class 变化
STYLE = 4, // 只有 style 变化
PROPS = 8, // 类/样式外的属性变化
FULL_PROPS = 16, // 需要完整 diff
HYDRATE_EVENTS = 32,
STABLE_FRAGMENT = 64,
KEYED_FRAGMENT = 128,
UNKEYED_FRAGMENT = 256,
NEED_PATCH = 512,
DYNAMIC_SLOTS = 1024,
// 标记 "无静态部分,直接完整 diff"
DEV_ROOT_FRAGMENT = 4096,
// 服务端渲染
DYNAMIC_SUSPENSE = 2048,
}示例
<div>
<span>{{ msg }}</span> <!-- PatchFlag: TEXT(1) -->
<span :class="active"> <!-- PatchFlag: CLASS(2) -->
<span :style="styleObj"> <!-- PatchFlag: STYLE(4) -->
<span :id="id"> <!-- PatchFlag: PROPS(8) -->
<span :title="a" :id="b"> <!-- PatchFlag: PROPS(8), dynamicProps = ['title', 'id'] -->
</div>运行时根据 patchFlag 走不同的分支:
function patchElement(oldVNode, newVNode) {
// 只处理带 patchFlag 的属性
if (patchFlag & PatchFlags.CLASS) {
// 只更新 class,跳过其他属性
hostPatchProp(el, 'class', oldProps.class, newProps.class)
}
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style)
}
if (patchFlag & PatchFlags.PROPS) {
// 只更新 dynamicProps 中列出的属性
for (const key of newVNode.dynamicProps) {
hostPatchProp(el, key, oldProps[key], newProps[key])
}
}
}没有 diff!Vue 3 直接跳过分发,走到精确的更新路径。
静态提升(hoistStatic)
模板中完全静态的部分被提升到渲染函数之外,只创建一次:
<template>
<div>
<span class="static">静态文本</span> <!-- 只创建一次 -->
<span>{{ dynamic }}</span> <!-- 每次重新渲染 -->
</div>
</template>编译后:
// 静态节点提升到函数外部
const _hoisted_1 = _createElementVNode('span', { class: 'static' }, '静态文本', -1 /* HOISTED */)
function render(_ctx, _cache) {
return _openBlock(), _createBlock('div', null, [
_hoisted_1, // 直接引用,不重新创建
_createElementVNode('span', null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
])
}标记
-1 (HOISTED)的节点永远不会参与 diff。
Block Tree
Vue 3 引入 Block Tree——以模板的根节点和所有动态节点为"骨架":
<template>
<div> <!-- Block 根 -->
<p>{{ msg }}</p> <!-- 动态节点 → 加入 dynamicChildren -->
<p>静态</p> <!-- 静态 → 不加入 -->
<div v-if="show"> <!-- 结构化指令 → 新 Block -->
<span>{{ name }}</span>
</div>
</div>
</template>// 生成的 render 函数
function render(_ctx, _cache) {
return (_openBlock(), _createBlock('div', null, [
_createElementVNode('p', null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
_createElementVNode('p', null, '静态'),
_ctx.show
? (_openBlock(), _createBlock('div', { key: 0 }, [
_createElementVNode('span', null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
: _createCommentVNode('v-if', true)
]))
}为什么快? 传统虚拟 DOM 需要递归比较整棵树。Block Tree 只比较 dynamicChildren 数组中的动态节点,静态节点完全跳过。
补丁过程
编译优化的收益
| 优化 | 效果 |
|---|---|
| patchFlag | 不需要 diff,直接知道改什么 |
| 静态提升 | 静态节点只创建一次,永不重渲染 |
| Block Tree | 跳过静态子节点,只追踪动态节点 |
| 事件缓存 | @click="handler" 在编译时缓存,不会变化 |
性能对比(相同场景下):Vue 3 虚拟 DOM 更新大约是 Vue 2 的 1.3~2 倍,且随模板静态内容比例增加,优势更大。
小结
| 概念 | 作用 |
|---|---|
| patchFlag | 编译时标记需要更新的内容类型 |
| 静态提升 | 纯静态节点只创建一次 |
| Block Tree | 只追踪动态节点,跳过静态区 |
| 编译时优化 | Vue 3 独特优势,减少运行时 diff |
上一节:响应式原理(Proxy) 下一节:实战项目:项目初始化与 UI 架构
