Vue3 Virtual DOM与Diff算法
约 2031 字大约 7 分钟
vue3virtual-domdiff
2025-07-27
概述
Vue 3 对 Virtual DOM 进行了彻底重写,引入了编译时优化(Compiler-Informed Virtual DOM),通过 PatchFlag、Block Tree、静态提升等技术,使运行时的 Diff 操作跳过大量静态内容,显著提升了渲染性能。
VNode 结构
// Vue 3 VNode 的简化结构
const vnode = {
type: 'div', // 标签名 / 组件 / Fragment
props: { // 属性
class: 'container',
onClick: handler
},
children: [], // 子节点
key: null, // 列表 key
// Vue 3 新增的优化字段
patchFlag: 1, // 标记动态内容类型
dynamicProps: ['class'],// 动态属性列表
dynamicChildren: [], // 动态子节点(Block 收集)
shapeFlag: 17, // 节点类型标志(位运算)
el: null, // 对应的真实 DOM
component: null, // 组件实例
};1. PatchFlag 优化
PatchFlag 是 Vue 3 编译器在编译模板时为动态节点添加的标记,告诉运行时只需要比较哪些部分。
// PatchFlag 定义
const PatchFlags = {
TEXT: 1, // 动态文本
CLASS: 2, // 动态 class
STYLE: 4, // 动态 style
PROPS: 8, // 动态非 class/style 属性
FULL_PROPS: 16, // 有动态 key 的属性
HYDRATE_EVENTS: 32,
STABLE_FRAGMENT: 64,
KEYED_FRAGMENT: 128,
UNKEYED_FRAGMENT: 256,
NEED_PATCH: 512, // ref / 指令
DYNAMIC_SLOTS: 1024,
HOISTED: -1, // 静态提升节点
BAIL: -2, // 退出优化模式
};编译示例
<template>
<div class="static-wrapper">
<h1>Static Title</h1>
<p :class="dynamicClass">{{ message }}</p>
<span>Static Content</span>
</div>
</template>编译器生成的渲染函数:
import { createVNode, toDisplayString, openBlock, createBlock } from 'vue';
// 静态节点在模块作用域提升,只创建一次
const _hoisted_1 = createVNode('h1', null, 'Static Title', -1 /* HOISTED */);
const _hoisted_2 = createVNode('span', null, 'Static Content', -1 /* HOISTED */);
function render(ctx) {
return (openBlock(), createBlock('div', { class: 'static-wrapper' }, [
_hoisted_1, // 静态,跳过 diff
createVNode('p', {
class: ctx.dynamicClass // 动态 class
}, toDisplayString(ctx.message), // 动态文本
3 /* TEXT | CLASS */), // patchFlag: 只比较 text 和 class
_hoisted_2 // 静态,跳过 diff
]));
}2. Block Tree
Block 是 Vue 3 的核心优化概念。每个 Block 节点收集其子树中所有动态节点到 dynamicChildren 数组中,Diff 时只遍历动态节点,跳过所有静态内容。
// openBlock / createBlock 工作原理
let currentBlock = null;
function openBlock() {
currentBlock = []; // 创建动态子节点收集数组
}
function createBlock(type, props, children) {
const vnode = createVNode(type, props, children);
vnode.dynamicChildren = currentBlock; // 挂载收集到的动态节点
currentBlock = null;
return vnode;
}
function createVNode(type, props, children, patchFlag) {
const vnode = { type, props, children, patchFlag };
if (patchFlag > 0 && currentBlock) {
currentBlock.push(vnode); // 动态节点注册到 Block
}
return vnode;
}Block 结构断裂
v-if 和 v-for 会创建新的 Block,因为它们改变了 DOM 结构:
<template>
<div> <!-- Block -->
<p>Static</p>
<div v-if="show"> <!-- 新 Block -->
<span>{{ text }}</span>
</div>
<ul v-for="item in list" :key="item.id"> <!-- 新 Block -->
<li>{{ item.name }}</li>
</ul>
</div>
</template>3. 静态提升(Static Hoisting)
Vue 3 编译器将静态节点提升到渲染函数外部,避免每次渲染重新创建。
// 未提升(每次渲染都创建新 VNode)
function render(ctx) {
return createVNode('div', null, [
createVNode('p', null, 'Static'), // 每次都创建
createVNode('span', null, ctx.msg),
createVNode('footer', null, 'Static Footer'), // 每次都创建
]);
}
// 静态提升后
const _hoisted_p = createVNode('p', null, 'Static');
const _hoisted_footer = createVNode('footer', null, 'Static Footer');
// 也可以提升静态 props
const _hoisted_props = { class: 'static-class', id: 'static-id' };
function render(ctx) {
return createVNode('div', null, [
_hoisted_p, // 复用,不重新创建
createVNode('span', null, ctx.msg),
_hoisted_footer, // 复用,不重新创建
]);
}4. Patch 函数与 Diff 流程
patchElement 核心逻辑
function patchElement(n1, n2) {
const el = (n2.el = n1.el); // 复用 DOM 元素
const oldProps = n1.props || {};
const newProps = n2.props || {};
const patchFlag = n2.patchFlag;
// 根据 patchFlag 精确更新
if (patchFlag > 0) {
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
el.className = newProps.class;
}
}
if (patchFlag & PatchFlags.STYLE) {
patchStyle(el, oldProps.style, newProps.style);
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
el.textContent = n2.children;
}
}
// 只比较标记的动态部分,跳过静态部分
} else if (patchFlag === 0) {
// 无标记,全量比较 props
patchProps(el, oldProps, newProps);
}
// Diff 子节点
if (n2.dynamicChildren) {
patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren);
} else {
patchChildren(n1, n2);
}
}5. 子节点 Diff 算法
Vue 3 使用最长递增子序列(LIS)算法优化列表节点的移动操作。
Diff 过程分五步
// Vue 3 子节点 Diff 简化实现
function patchKeyedChildren(c1, c2, container) {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1;
let e2 = l2 - 1;
// Step 1: 同步头部
// (a b) c d
// (a b) e c d
while (i <= e1 && i <= e2) {
if (isSameVNodeType(c1[i], c2[i])) {
patch(c1[i], c2[i], container);
} else {
break;
}
i++;
}
// Step 2: 同步尾部
// a b (c d)
// a e b (c d)
while (i <= e1 && i <= e2) {
if (isSameVNodeType(c1[e1], c2[e2])) {
patch(c1[e1], c2[e2], container);
} else {
break;
}
e1--;
e2--;
}
// Step 3: 新增节点
if (i > e1 && i <= e2) {
while (i <= e2) {
mount(c2[i], container);
i++;
}
}
// Step 4: 删除节点
else if (i > e2 && i <= e1) {
while (i <= e1) {
unmount(c1[i]);
i++;
}
}
// Step 5: 未知序列(使用 LIS)
else {
patchUnknownSequence(c1, c2, i, e1, e2, container);
}
}最长递增子序列(LIS)
// LIS 用于最小化 DOM 移动操作
// 旧序列: a b c d e f g
// 新序列: a b e c d h f g
// 去掉相同前缀 (a b) 和后缀 (f g):
// 旧: c d e
// 新: e c d h
// 新序列中各元素在旧序列中的索引: [4, 2, 3, -1]
// e→4, c→2, d→3, h→-1(新增)
// LIS: [2, 3] 对应 c, d(不需要移动)
// e 需要移动,h 需要新增
function getSequence(arr) {
const n = arr.length;
const result = [0];
const p = new Array(n);
for (let i = 1; i < n; i++) {
const val = arr[i];
if (val === -1) continue; // 跳过新增节点
const last = arr[result[result.length - 1]];
if (val > last) {
p[i] = result[result.length - 1];
result.push(i);
continue;
}
// 二分查找
let lo = 0, hi = result.length - 1;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (arr[result[mid]] < val) lo = mid + 1;
else hi = mid;
}
if (val < arr[result[lo]]) {
if (lo > 0) p[i] = result[lo - 1];
result[lo] = i;
}
}
// 回溯
let len = result.length;
let last = result[len - 1];
while (len-- > 0) {
result[len] = last;
last = p[last];
}
return result;
}6. Fragment / Teleport / Suspense
Vue 3 引入了三种特殊的内置组件:
<!-- Fragment: 多根节点 -->
<template>
<h1>Title</h1>
<p>Content</p>
<!-- Vue 3 自动包装为 Fragment VNode -->
</template>
<!-- Teleport: 传送节点到 DOM 其他位置 -->
<template>
<Teleport to="body">
<div class="modal">Modal Content</div>
</Teleport>
</template>
<!-- Suspense: 异步组件加载 -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Loading />
</template>
</Suspense>
</template>7. 与 React Diff 的对比
| 对比维度 | Vue 3 | React |
|---|---|---|
| 优化时机 | 编译时 + 运行时 | 运行时 |
| 静态内容 | 通过 PatchFlag/Block 跳过 | 全量遍历 |
| 列表 Diff | 双端比较 + LIS | 单向遍历 + Map |
| 移动策略 | 最长递增子序列(最优) | lastPlacedIndex(可能多移动) |
| 适用场景 | 模板编译友好 | JSX 灵活性更高 |
总结
Vue 3 的 Virtual DOM 通过编译器与运行时的深度协作,实现了极高的 Diff 效率。PatchFlag 精确标记动态内容类型,Block Tree 使 Diff 只处理动态节点,静态提升避免重复创建 VNode,最长递增子序列算法最小化 DOM 移动操作。这种 "Compiler-Informed Virtual DOM" 策略在保留 Virtual DOM 灵活性的同时,达到了接近手写 DOM 操作的性能水平。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于