Skip to main content

Vue2与Vue3的Diff算法对比

· 7 min read
LIU

1. 虚拟 DOM 基础

虚拟 DOM 结构

Vue2 和 Vue3 的虚拟 DOM 结构有所不同:

// Vue2 的虚拟 DOM 结构
{
tag: 'div',
data: {
attrs: { id: 'app' },
on: { click: handler }
},
children: [
{
tag: 'span',
data: { attrs: { class: 'text' } },
children: ['Hello']
}
]
}

// Vue3 的虚拟 DOM 结构
{
type: 'div',
props: {
id: 'app',
onClick: handler
},
children: [
{
type: 'span',
props: { class: 'text' },
children: ['Hello']
}
]
}

主要区别

  1. Vue3 使用 type 替代 tag 表示节点类型
  2. Vue3 扁平化处理 props,不再区分 attrs 和 on
  3. Vue3 的虚拟 DOM 结构更加简洁,减少了内存占用

2. Diff 算法核心流程

Vue2 的 Diff 算法

Vue2 的 Diff 算法采用双端比较的方式:

function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let newStartIdx = 0;
let newEndIdx = newCh.length - 1;

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVnode(oldCh[oldStartIdx], newCh[newStartIdx])) {
patchVnode(oldCh[oldStartIdx], newCh[newStartIdx]);
oldStartIdx++;
newStartIdx++;
} else if (sameVnode(oldCh[oldEndIdx], newCh[newEndIdx])) {
patchVnode(oldCh[oldEndIdx], newCh[newEndIdx]);
oldEndIdx--;
newEndIdx--;
} else if (sameVnode(oldCh[oldStartIdx], newCh[newEndIdx])) {
patchVnode(oldCh[oldStartIdx], newCh[newEndIdx]);
parentElm.insertBefore(
oldCh[oldStartIdx].elm,
oldCh[oldEndIdx].elm.nextSibling
);
oldStartIdx++;
newEndIdx--;
} else if (sameVnode(oldCh[oldEndIdx], newCh[newStartIdx])) {
patchVnode(oldCh[oldEndIdx], newCh[newStartIdx]);
parentElm.insertBefore(oldCh[oldEndIdx].elm, oldCh[oldStartIdx].elm);
oldEndIdx--;
newStartIdx++;
} else {
// 创建 key 到 index 的映射
const idxInOld = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
const idxInNew = newCh[newStartIdx].key;

if (idxInOld[idxInNew]) {
// 移动节点
const vnodeToMove = oldCh[idxInOld[idxInNew]];
patchVnode(vnodeToMove, newCh[newStartIdx]);
oldCh[idxInOld[idxInNew]] = undefined;
parentElm.insertBefore(vnodeToMove.elm, oldCh[oldStartIdx].elm);
} else {
// 创建新节点
createElm(newCh[newStartIdx], parentElm, oldCh[oldStartIdx].elm);
}
newStartIdx++;
}
}
}

Vue3 的 Diff 算法

Vue3 采用快速 Diff 算法,主要优化点:

function patchKeyedChildren(c1, c2, container) {
let i = 0;
let e1 = c1.length - 1;
let e2 = c2.length - 1;

// 1. 从头部开始比较
while (i <= e1 && i <= e2 && c1[i].key === c2[i].key) {
patch(c1[i], c2[i], container);
i++;
}

// 2. 从尾部开始比较
while (i <= e1 && i <= e2 && c1[e1].key === c2[e2].key) {
patch(c1[e1], c2[e2], container);
e1--;
e2--;
}

// 3. 处理新增和删除的节点
if (i > e1) {
// 新增节点
const nextPos = e2 + 1;
const anchor = nextPos < c2.length ? c2[nextPos].el : null;
while (i <= e2) {
patch(null, c2[i], container, anchor);
i++;
}
} else if (i > e2) {
// 删除节点
while (i <= e1) {
unmount(c1[i]);
i++;
}
} else {
// 4. 处理未知序列
const s1 = i;
const s2 = i;
const keyToNewIndexMap = new Map();

// 建立新子节点的 key 到 index 的映射
for (let i = s2; i <= e2; i++) {
keyToNewIndexMap.set(c2[i].key, i);
}

// 更新和移动节点
let moved = false;
let maxNewIndexSoFar = 0;
const toBePatched = e2 - s2 + 1;
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);

for (let 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);
}
}

// 移动节点
if (moved) {
const seq = getSequence(newIndexToOldIndexMap);
let j = seq.length - 1;
for (let i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1].el : null;

if (newIndexToOldIndexMap[i] === 0) {
patch(null, nextChild, container, anchor);
} else if (moved) {
if (j < 0 || i !== seq[j]) {
move(nextChild, container, anchor);
} else {
j--;
}
}
}
}
}
}

3. 主要优化点

静态标记

Vue3 引入了静态标记(PatchFlag),用于标记动态内容:

// Vue3 的静态标记示例
const vnode = {
type: "div",
props: {
id: "app",
class: "container",
},
children: [
{
type: "span",
props: { class: "text" },
children: ["Hello"],
patchFlag: 1, // 表示文本内容可能变化
},
],
};

静态提升

Vue3 将静态节点提升到渲染函数之外:

// Vue3 的静态提升
const hoisted = createVNode("div", null, "静态内容");

function render() {
return createVNode("div", null, [
hoisted,
createVNode("span", null, state.message),
]);
}

事件监听器缓存

Vue3 缓存事件监听器,避免重复创建:

// Vue3 的事件监听器缓存
const render = (ctx) => {
return createVNode(
"button",
{
onClick: ctx.onClick, // 事件监听器被缓存
},
"点击"
);
};

4. 性能对比

时间复杂度

  • Vue2: O(n²)
  • Vue3: O(n)

内存占用

  • Vue2: 较大的内存占用
  • Vue3: 显著减少的内存占用

实际性能提升

  1. 首次渲染性能提升约 50%
  2. 更新性能提升约 100%
  3. 内存占用减少约 50%

5. 最佳实践

合理使用 key

<!-- 推荐:使用唯一且稳定的 key -->
<template v-for="item in items" :key="item.id">
<div>{{ item.name }}</div>
</template>

<!-- 不推荐:使用索引作为 key -->
<template v-for="(item, index) in items" :key="index">
<div>{{ item.name }}</div>
</template>

避免不必要的更新

// 使用 shallowRef 减少响应式开销
const state = shallowRef({
count: 0,
user: {
name: "John",
},
});

// 使用 markRaw 标记不需要响应式的对象
const staticData = markRaw({
config: {
theme: "dark",
},
});

合理使用组件

<!-- 使用异步组件减少初始加载时间 -->
const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue')
)

<!-- 使用 keep-alive 缓存组件状态 -->
<keep-alive>
<component :is="currentComponent" />
</keep-alive>

6. 总结

Vue3 的 Diff 算法相比 Vue2 有了显著的改进:

  1. 采用快速 Diff 算法,时间复杂度从 O(n²) 降低到 O(n)
  2. 引入静态标记和静态提升,减少不必要的更新
  3. 优化事件监听器,减少内存占用
  4. 提供更多性能优化 API,如 shallowRef、markRaw 等