跳到主要内容

Vue 虚拟 DOM 与 Diff 算法

问题

Vue 的虚拟 DOM 是什么?Diff 算法是如何工作的?key 在其中起什么作用?

答案

什么是虚拟 DOM

虚拟 DOM(Virtual DOM)是用 JavaScript 对象来描述真实 DOM 结构的抽象层。Vue 通过对比新旧虚拟 DOM 树的差异(Diff),最小化真实 DOM 操作。

// 真实 DOM
// <div class="container">
// <span>Hello</span>
// </div>

// 虚拟 DOM(VNode)
interface VNode {
type: string | Component; // 标签名或组件
props: Record<string, any>; // 属性
children: VNode[] | string; // 子节点
key: string | number | null; // 唯一标识
el: Element | null; // 对应的真实 DOM
}

const vnode: VNode = {
type: 'div',
props: { class: 'container' },
children: [
{
type: 'span',
props: null,
children: 'Hello',
key: null,
el: null
}
],
key: null,
el: null
};

虚拟 DOM 的优势

优势说明
跨平台不直接依赖 DOM API,可渲染到不同平台
批量更新收集多次变更,一次性更新 DOM
最小化 DOM 操作通过 Diff 算法只更新变化的部分
声明式编程开发者关注数据,框架处理 DOM 更新
虚拟 DOM 不一定更快

虚拟 DOM 的优势在于开发效率可维护性,而非绝对性能。简单场景下,直接操作 DOM 可能更快。

Diff 算法核心策略

Vue 3 的 Diff 算法采用同层比较策略,时间复杂度从 O(n3)O(n^3) 优化到 O(n)O(n)

三大假设

  1. 跨层级移动极少 → 只比较同层节点
  2. 相同类型才复用 → 类型不同直接替换
  3. key 标识唯一性 → 通过 key 快速定位节点

patch 函数核心逻辑

function patch(
n1: VNode | null, // 旧节点
n2: VNode, // 新节点
container: Element
) {
// 1. 旧节点不存在 → 挂载新节点
if (!n1) {
mount(n2, container);
return;
}

// 2. 类型不同 → 卸载旧节点,挂载新节点
if (n1.type !== n2.type) {
unmount(n1);
mount(n2, container);
return;
}

// 3. 类型相同 → 更新属性和子节点
const el = (n2.el = n1.el!);

// 更新 props
patchProps(el, n1.props, n2.props);

// 更新 children
patchChildren(n1, n2, el);
}

patchChildren - 子节点 Diff

子节点有三种类型:文本、数组、空,组合成 9 种情况:

function patchChildren(
n1: VNode,
n2: VNode,
container: Element
) {
const c1 = n1.children;
const c2 = n2.children;

// 新子节点是文本
if (typeof c2 === 'string') {
if (Array.isArray(c1)) {
// 卸载旧的子节点数组
c1.forEach(child => unmount(child));
}
container.textContent = c2;
return;
}

// 新子节点是数组
if (Array.isArray(c2)) {
if (Array.isArray(c1)) {
// 核心:双端 Diff 算法
patchKeyedChildren(c1, c2, container);
} else {
container.textContent = '';
c2.forEach(child => mount(child, container));
}
return;
}

// 新子节点为空
if (Array.isArray(c1)) {
c1.forEach(child => unmount(child));
} else if (typeof c1 === 'string') {
container.textContent = '';
}
}

双端 Diff 算法(Vue 2)

Vue 2 使用双端比较算法,同时从新旧节点的两端进行比较:

function patchKeyedChildren(
c1: VNode[],
c2: VNode[],
container: Element
) {
let oldStartIdx = 0;
let oldEndIdx = c1.length - 1;
let newStartIdx = 0;
let newEndIdx = c2.length - 1;

let oldStartVNode = c1[oldStartIdx];
let oldEndVNode = c1[oldEndIdx];
let newStartVNode = c2[newStartIdx];
let newEndVNode = c2[newEndIdx];

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVNode(oldStartVNode, newStartVNode)) {
// 头头相同
patch(oldStartVNode, newStartVNode, container);
oldStartVNode = c1[++oldStartIdx];
newStartVNode = c2[++newStartIdx];
} else if (sameVNode(oldEndVNode, newEndVNode)) {
// 尾尾相同
patch(oldEndVNode, newEndVNode, container);
oldEndVNode = c1[--oldEndIdx];
newEndVNode = c2[--newEndIdx];
} else if (sameVNode(oldStartVNode, newEndVNode)) {
// 头尾相同:旧头移动到旧尾后面
patch(oldStartVNode, newEndVNode, container);
container.insertBefore(oldStartVNode.el!, oldEndVNode.el!.nextSibling);
oldStartVNode = c1[++oldStartIdx];
newEndVNode = c2[--newEndIdx];
} else if (sameVNode(oldEndVNode, newStartVNode)) {
// 尾头相同:旧尾移动到旧头前面
patch(oldEndVNode, newStartVNode, container);
container.insertBefore(oldEndVNode.el!, oldStartVNode.el!);
oldEndVNode = c1[--oldEndIdx];
newStartVNode = c2[++newStartIdx];
} else {
// 四种比较都不匹配,使用 key 查找
const idxInOld = c1.findIndex(
node => node.key === newStartVNode.key
);
if (idxInOld >= 0) {
const vnodeToMove = c1[idxInOld];
patch(vnodeToMove, newStartVNode, container);
container.insertBefore(vnodeToMove.el!, oldStartVNode.el!);
c1[idxInOld] = undefined as any; // 标记已处理
} else {
// 新节点,挂载
mount(newStartVNode, container, oldStartVNode.el!);
}
newStartVNode = c2[++newStartIdx];
}
}

// 处理剩余节点
if (oldStartIdx > oldEndIdx) {
// 旧节点遍历完,新节点有剩余 → 新增
for (let i = newStartIdx; i <= newEndIdx; i++) {
mount(c2[i], container, c2[newEndIdx + 1]?.el);
}
} else if (newStartIdx > newEndIdx) {
// 新节点遍历完,旧节点有剩余 → 删除
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (c1[i]) unmount(c1[i]);
}
}
}

function sameVNode(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key;
}

快速 Diff 算法(Vue 3)

Vue 3 采用更高效的快速 Diff 算法,结合最长递增子序列(LIS)

function patchKeyedChildrenFast(
c1: VNode[],
c2: VNode[],
container: Element
) {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1;
let e2 = l2 - 1;

// 1. 从头部开始同步
while (i <= e1 && i <= e2) {
if (sameVNode(c1[i], c2[i])) {
patch(c1[i], c2[i], container);
i++;
} else {
break;
}
}

// 2. 从尾部开始同步
while (i <= e1 && i <= e2) {
if (sameVNode(c1[e1], c2[e2])) {
patch(c1[e1], c2[e2], container);
e1--;
e2--;
} else {
break;
}
}

// 3. 旧节点遍历完,有新节点需要挂载
if (i > e1 && i <= e2) {
const anchor = c2[e2 + 1]?.el || null;
while (i <= e2) {
mount(c2[i], container, anchor);
i++;
}
}
// 4. 新节点遍历完,有旧节点需要卸载
else if (i > e2 && i <= e1) {
while (i <= e1) {
unmount(c1[i]);
i++;
}
}
// 5. 中间部分乱序,使用最长递增子序列优化
else {
const s1 = i;
const s2 = i;

// 建立新节点 key -> index 的映射
const keyToNewIndexMap = new Map<any, number>();
for (i = s2; i <= e2; i++) {
keyToNewIndexMap.set(c2[i].key, i);
}

// 记录新节点在旧节点中的位置
const toBePatched = e2 - s2 + 1;
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
let moved = false;
let maxNewIndexSoFar = 0;

for (i = s1; i <= e1; i++) {
const oldVNode = c1[i];
const newIndex = keyToNewIndexMap.get(oldVNode.key);

if (newIndex === undefined) {
unmount(oldVNode);
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1;
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
patch(oldVNode, c2[newIndex], container);
}
}

// 使用最长递增子序列最小化移动
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];

let j = increasingNewIndexSequence.length - 1;
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const anchor = c2[nextIndex + 1]?.el || null;

if (newIndexToOldIndexMap[i] === 0) {
// 新节点,挂载
mount(c2[nextIndex], container, anchor);
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
// 需要移动
container.insertBefore(c2[nextIndex].el!, anchor);
} else {
// 在递增子序列中,不需要移动
j--;
}
}
}
}
}

// 最长递增子序列算法
function getSequence(arr: number[]): number[] {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;

for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}

key 的作用

// ❌ 不使用 key:节点复用错误,性能差
// 旧: [A, B, C]
// 新: [C, A, B]
// Diff 会认为 ABC 都需要更新内容

// ✅ 使用 key:正确识别节点,只需移动
// 旧: [A:1, B:2, C:3]
// 新: [C:3, A:1, B:2]
// Diff 识别出只需要移动 C 到最前面
不要用 index 作为 key
<!-- ❌ 错误:index 会随数组变化 -->
<li v-for="(item, index) in list" :key="index">{{ item }}</li>

<!-- ✅ 正确:使用唯一标识 -->
<li v-for="item in list" :key="item.id">{{ item }}</li>

常见面试问题

Q1: Vue 2 和 Vue 3 的 Diff 算法有什么区别?

答案

特性Vue 2Vue 3
算法双端 Diff快速 Diff
核心优化头尾双指针最长递增子序列
静态提升
静态标记✅ PatchFlags

Vue 3 的快速 Diff 优势:

  1. 预处理相同前缀/后缀:先处理头尾不变的节点
  2. 最长递增子序列:最小化 DOM 移动操作
  3. 静态提升:静态节点跳过 Diff
  4. PatchFlags:精确标记动态内容类型

Q2: key 为什么不能用 index?

答案

使用 index 作为 key 会导致:

// 原始数据: [{id: 1, name: 'A'}, {id: 2, name: 'B'}]
// key 使用 index: [A:0, B:1]

// 删除第一项后: [{id: 2, name: 'B'}]
// key 变成: [B:0]

// Diff 认为:key=0 的节点内容从 A 变成 B
// 实际上应该:删除 A 节点,复用 B 节点

问题:

  1. 错误的节点复用:状态混乱
  2. 性能下降:本可以复用的节点被更新
  3. 表单输入丢失:输入框等组件状态错乱

Q3: 虚拟 DOM 一定比直接操作 DOM 快吗?

答案

不一定。虚拟 DOM 有额外开销:

  1. 创建 VNode 对象
  2. Diff 对比
  3. 最终还是要操作 DOM
// 简单场景:直接操作 DOM 更快
element.textContent = 'new text';

// 复杂场景:虚拟 DOM 优势明显
// - 批量更新
// - 最小化重排重绘
// - 跨平台渲染

虚拟 DOM 的价值在于开发效率而非绝对性能。

Q4: Vue 3 的静态提升是什么?

答案

静态节点在编译时提升到渲染函数外部,避免重复创建:

<template>
<div>
<span>静态文本</span>
<span>{{ dynamic }}</span>
</div>
</template>

编译后:

// 静态提升到外部
const _hoisted_1 = /*#__PURE__*/ createVNode("span", null, "静态文本");

function render() {
return createVNode("div", null, [
_hoisted_1, // 直接复用
createVNode("span", null, ctx.dynamic, 1 /* TEXT */)
]);
}

Q5: Vue 3 的快速 Diff 算法和 Vue 2 的双端 Diff 有什么区别?

答案

两者在核心策略和优化手段上有显著差异。Vue 3 的快速 Diff 在保留双端思想的基础上,引入了预处理 + 最长递增子序列(LIS),大幅减少 DOM 移动次数。

算法流程对比

具体差异

维度Vue 2 双端 DiffVue 3 快速 Diff
比较方式头头、尾尾、头尾、尾头四次比较先处理相同前缀和后缀
查找策略遍历查找 key(O(n)O(n)构建 key → index Map(O(1)O(1) 查找)
移动优化逐个判断是否需要移动最长递增子序列,批量确定不动节点
最坏复杂度O(n2)O(n^2)(无 key 时遍历查找)O(nlogn)O(n \log n)(LIS 的时间复杂度)
编译优化配合PatchFlags + 静态提升

示例:节点移动

假设旧列表为 [A, B, C, D, E],新列表为 [A, D, B, C, E]

// Vue 2 双端 Diff:
// 1. 头头比较:A === A → patch,两端指针内移
// 2. 尾尾比较:E === E → patch,两端指针内移
// 3. 剩余 [B, C, D] vs [D, B, C]
// 4. 头头:B !== D → 头尾:B !== C → 尾头:D === D → 移动 D 到 B 前面
// 5. 头头:B === B → patch
// 6. 头头:C === C → patch
// 总共移动:1 次

// Vue 3 快速 Diff:
// 1. 预处理前缀:A === A → patch
// 2. 预处理后缀:E === E → patch
// 3. 剩余中间部分 [B, C, D] vs [D, B, C]
// 4. 构建 key→index 映射:{ D: 1, B: 2, C: 3 }
// 5. newIndexToOldIndexMap: [3, 1, 2](D 在旧索引 3,B 在 1,C 在 2)
// 6. 最长递增子序列:[1, 2](B, C 不需要移动)
// 7. 只移动 D 到 B 前面
// 总共移动:1 次,但判断过程更高效
核心优势

Vue 3 快速 Diff 通过最长递增子序列一次性确定哪些节点不需要移动,避免了 Vue 2 双端 Diff 中逐步试探的过程。在大量节点乱序移动的场景下,Vue 3 的移动次数更少,性能更优。

Q6: 什么是静态提升(Static Hoisting)?对性能有什么影响?

答案

静态提升是 Vue 3 编译器的优化策略,将模板中不会变化的静态节点和属性提升到渲染函数外部,避免每次渲染时重复创建。

优化前后对比

<template>
<div>
<h1 class="title">固定标题</h1>
<p>固定段落</p>
<span>{{ message }}</span>
<ul>
<li class="item">静态项 1</li>
<li class="item">静态项 2</li>
<li class="item">静态项 3</li>
</ul>
</div>
</template>

未优化(每次渲染都重新创建所有 VNode):

function render(ctx: { message: string }) {
return createVNode('div', null, [
createVNode('h1', { class: 'title' }, '固定标题'),
createVNode('p', null, '固定段落'),
createVNode('span', null, ctx.message, 1 /* TEXT */),
createVNode('ul', null, [
createVNode('li', { class: 'item' }, '静态项 1'),
createVNode('li', { class: 'item' }, '静态项 2'),
createVNode('li', { class: 'item' }, '静态项 3'),
]),
]);
}

静态提升后

// 静态节点提升到模块作用域,只创建一次
const _hoisted_1 = createVNode('h1', { class: 'title' }, '固定标题');
const _hoisted_2 = createVNode('p', null, '固定段落');
const _hoisted_3 = createVNode('ul', null, [
createVNode('li', { class: 'item' }, '静态项 1'),
createVNode('li', { class: 'item' }, '静态项 2'),
createVNode('li', { class: 'item' }, '静态项 3'),
]);

function render(ctx: { message: string }) {
return createVNode('div', null, [
_hoisted_1, // 直接复用
_hoisted_2, // 直接复用
createVNode('span', null, ctx.message, 1 /* TEXT */),
_hoisted_3, // 直接复用(整棵子树)
]);
}

提升层级

提升类型说明示例
静态节点提升纯静态节点提升为常量<h1>标题</h1>
静态属性提升静态 props 提升为常量{ class: 'title' }
静态子树提升整棵静态子树提升多个连续静态节点
字符串化连续静态节点编译为 HTML 字符串超过阈值时触发

字符串化优化

当连续静态节点数量超过阈值(默认 20 个)时,Vue 3 会将它们编译为 HTML 字符串,通过 innerHTML 一次性插入:

// 大量静态节点 → 字符串化
const _hoisted_1 = /*#__PURE__*/ createStaticVNode(
'<li class="item">项目 1</li><li class="item">项目 2</li>...(20+个)',
20
);

性能影响

如何验证

Vue 3 Template Explorer 中,可以勾选 hoistStatic 选项实时查看编译器输出的静态提升效果。

Q7: Patch Flags 是什么?如何优化 Diff 过程?

答案

Patch Flags(补丁标记)是 Vue 3 编译器在编译阶段为动态节点生成的标记,告诉运行时 Diff 算法"这个节点的哪些部分是动态的",从而跳过不必要的比较。

标记类型

// Vue 3 源码中的 PatchFlags 枚举
enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态非 class/style 的 props
FULL_PROPS = 1 << 4, // 有动态 key 的 props(需要全量 Diff)
NEED_HYDRATION = 1 << 5, // 需要 hydration 的事件监听
STABLE_FRAGMENT = 1 << 6, // 子节点顺序稳定的 Fragment
KEYED_FRAGMENT = 1 << 7, // 有 key 的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 无 key 的 Fragment
NEED_PATCH = 1 << 9, // 非 props 的动态绑定(如 ref、指令)
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
// 以下为特殊标记(负数),不参与位运算优化
HOISTED = -1, // 静态提升的节点
BAIL = -2, // Diff 应退出优化模式
}

编译示例

<template>
<div>
<!-- 纯静态 -->
<h1>标题</h1>

<!-- 动态文本 → PatchFlag: 1 (TEXT) -->
<span>{{ msg }}</span>

<!-- 动态 class → PatchFlag: 2 (CLASS) -->
<div :class="{ active: isActive }">内容</div>

<!-- 动态 style → PatchFlag: 4 (STYLE) -->
<p :style="{ color: textColor }">段落</p>

<!-- 动态文本 + 动态 class → PatchFlag: 3 (TEXT | CLASS) -->
<span :class="cls">{{ text }}</span>

<!-- 动态 props → PatchFlag: 8 (PROPS),附带动态属性名 -->
<img :src="imgUrl" :alt="imgAlt" />
</div>
</template>

编译结果:

const _hoisted_1 = createVNode('h1', null, '标题', -1 /* HOISTED */);

function render(ctx: Record<string, any>) {
return createVNode('div', null, [
_hoisted_1,
createVNode('span', null, ctx.msg, 1 /* TEXT */),
createVNode('div', { class: { active: ctx.isActive } }, '内容', 2 /* CLASS */),
createVNode('p', { style: { color: ctx.textColor } }, '段落', 4 /* STYLE */),
createVNode('span', { class: ctx.cls }, ctx.text, 3 /* TEXT | CLASS */),
createVNode('img', { src: ctx.imgUrl, alt: ctx.imgAlt }, null, 8 /* PROPS */, ['src', 'alt']),
]);
}

Diff 时如何利用 Patch Flags

function patchElement(n1: VNode, n2: VNode) {
const el = (n2.el = n1.el!);
const patchFlag = n2.patchFlag;

// 有 PatchFlag → 精确更新
if (patchFlag > 0) {
if (patchFlag & PatchFlags.TEXT) {
// 只更新文本内容
if (n1.children !== n2.children) {
el.textContent = n2.children as string;
}
}

if (patchFlag & PatchFlags.CLASS) {
// 只更新 class
if (n1.props?.class !== n2.props?.class) {
el.className = n2.props?.class;
}
}

if (patchFlag & PatchFlags.STYLE) {
// 只更新 style
patchStyle(el, n1.props?.style, n2.props?.style);
}

if (patchFlag & PatchFlags.PROPS) {
// 只 Diff 指定的动态属性
const dynamicProps = n2.dynamicProps!;
for (const key of dynamicProps) {
if (n1.props?.[key] !== n2.props?.[key]) {
el.setAttribute(key, n2.props?.[key]);
}
}
}
} else if (patchFlag === PatchFlags.FULL_PROPS) {
// 需要全量 Diff props
patchProps(el, n1.props, n2.props);
}

// PatchFlag 为 -1 (HOISTED) 的节点直接跳过
}

Block Tree 与动态节点收集

Vue 3 还引入了 Block Tree,在编译时将模板中所有动态节点收集到一个扁平数组中,Diff 时只遍历动态节点,跳过整棵静态子树:

// 编译器生成的 Block
function render(ctx: Record<string, any>) {
return (
openBlock(),
createBlock('div', null, [
_hoisted_1, // 静态节点,不进入 dynamicChildren
_hoisted_2,
createVNode('span', null, ctx.msg, 1 /* TEXT */), // 动态节点
createVNode('div', { class: ctx.cls }, '内容', 2 /* CLASS */), // 动态节点
])
);
// Block 的 dynamicChildren = [span, div](只有动态节点)
}
性能提升总结
优化手段效果
PatchFlags精确知道节点的哪些部分是动态的,避免全量比较
Block Tree将 Diff 范围从整棵树缩小到动态节点集合
位运算标记patchFlag & PatchFlags.TEXT 高效判断,O(1)O(1)
静态提升静态节点标记为 -1,Diff 时完全跳过

Q8: 虚拟 DOM 一定比直接操作 DOM 快吗?什么场景下不适合用虚拟 DOM?

答案

虚拟 DOM 不一定比直接操作 DOM 快。虚拟 DOM 的本质是用 JavaScript 运算(创建 VNode、Diff 对比)换取最小化的 DOM 操作,它提供的是一个性能下限有保障的方案,而非最优方案。

性能公式对比

虚拟 DOM 总开销=创建 VNode+Diff 对比+必要的 DOM 操作\text{虚拟 DOM 总开销} = \text{创建 VNode} + \text{Diff 对比} + \text{必要的 DOM 操作} 直接操作 DOM=DOM 操作\text{直接操作 DOM} = \text{DOM 操作}

当 DOM 操作本身很简单时,虚拟 DOM 额外的 JavaScript 运算反而成为负担。

虚拟 DOM 不占优的场景

场景一:简单的文本更新

// 直接操作 DOM:1 次操作
element.textContent = 'new text';

// 虚拟 DOM:
// 1. 创建新的 VNode 树
// 2. Diff 对比新旧 VNode
// 3. 发现文本变化
// 4. element.textContent = 'new text'
// 多了步骤 1-3 的开销

场景二:已知精确变更的场景

// 开发者明确知道哪里需要更新
function updatePrice(el: HTMLElement, price: number) {
el.querySelector('.price')!.textContent = `¥${price}`;
el.querySelector('.price')!.classList.toggle('expensive', price > 1000);
}

// 虚拟 DOM 需要 Diff 整棵组件树来发现这两处变更

场景三:高频动画 / Canvas 渲染

// Canvas 动画:直接操作更高效
function animate(ctx: CanvasRenderingContext2D) {
requestAnimationFrame(() => {
ctx.clearRect(0, 0, 800, 600);
// 直接绘制,无需虚拟 DOM
particles.forEach((p) => {
ctx.fillRect(p.x, p.y, 2, 2);
});
animate(ctx);
});
}

场景四:极大规模 DOM 节点

当节点数量达到数万级别时,即使有 Diff 优化,VNode 对象本身的内存分配和垃圾回收也会带来压力。此时应使用虚拟滚动等方案减少节点数量。

虚拟 DOM 的真正优势

优势说明
开发效率声明式编程,开发者不需要手动追踪每个变更
性能下限自动 Diff 保证不会出现灾难性的 DOM 操作
跨平台相同逻辑可渲染到 DOM、Canvas、Native、SSR 字符串
批量更新合并多次数据变更为一次 DOM 更新
可预测性相同数据总是产生相同的 UI,便于调试和测试

性能对比图

面试关键点

面试中回答这个问题时,不要简单说"虚拟 DOM 更快"或"更慢",应强调以下观点:

  1. 虚拟 DOM 是性能与开发效率的平衡方案
  2. 它保证了不错的性能下限,而非最优性能上限
  3. Vue 3 通过编译优化(PatchFlags、静态提升、Block Tree)进一步缩小了虚拟 DOM 与手动操作的性能差距
  4. 在复杂应用中,虚拟 DOM 的自动 Diff + 最小化更新通常比开发者手动管理 DOM 更可靠
尤雨溪对虚拟 DOM 性能的经典论述

框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。框架给你的保证是,我在不需要你手动优化的情况下,依然可以给你提供过得去的性能。

重点是"过得去的性能" + "更好的开发体验",而不是"最快"。

相关链接