Vue diff算法完全指南 - 原理详解与Vue2/Vue3对比

题目 ​请详细说明Vue的diff算法原理,包括key的作用,并对比Vue2和Vue3的diff算法有什么区别和优化?

📝 标准答案 ​核心要点 ​diff算法目的:

高效对比新旧虚拟DOM,最小化真实DOM操作将传统O(n³)复杂度优化到O(n)通过同层比较和key标识实现高效更新Vue2双端比较算法:

同时从头尾两端开始比较(头头、尾尾、头尾、尾头)时间复杂度O(n),适合简单的增删改操作在复杂移动场景下可能产生不必要的移动Vue3最长递增子序列算法:

基于最长递增子序列(LIS)优化,减少移动操作预处理头尾相同节点,只对中间部分使用LIS复杂场景下性能显著优于Vue2key的作用:

唯一标识节点,帮助Vue正确识别元素变化避免就地复用导致的状态错乱问题提升diff算法的准确性和性能详细说明 ​什么是diff算法? ​diff算法是虚拟DOM的核心,用于对比新旧虚拟DOM树的差异,然后高效地更新真实DOM。

为什么需要diff算法?

直接操作DOM很慢,需要调用浏览器API重新创建整个DOM树代价太大需要找出最小的变化集合,减少DOM操作保持组件状态(如input焦点、滚动位置等)javascript// 虚拟DOM结构

const vnode = {

tag: 'div',

props: { class: 'container' },

children: [

{ tag: 'p', children: 'Hello' },

{ tag: 'p', children: 'World' }

]

};

// diff算法比较新旧虚拟DOM

function patch(oldVNode, newVNode) {

// 1. 如果节点类型不同,直接替换

if (oldVNode.tag !== newVNode.tag) {

return replaceNode(oldVNode, newVNode);

}

// 2. 如果节点类型相同,更新属性

patchProps(oldVNode, newVNode);

// 3. 比较子节点(核心diff算法)

patchChildren(oldVNode.children, newVNode.children);

}1234567891011121314151617181920212223Vue2的diff算法(双端比较) ​Vue2使用双端比较算法,同时从新旧节点列表的头尾四个位置开始比较:

javascript// Vue2双端比较算法(简化版)

function updateChildren(oldCh, newCh) {

let oldStartIdx = 0;

let oldEndIdx = oldCh.length - 1;

let newStartIdx = 0;

let newEndIdx = newCh.length - 1;

let oldStartVNode = oldCh[0];

let oldEndVNode = oldCh[oldEndIdx];

let newStartVNode = newCh[0];

let newEndVNode = newCh[newEndIdx];

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

if (!oldStartVNode) {

oldStartVNode = oldCh[++oldStartIdx];

} else if (!oldEndVNode) {

oldEndVNode = oldCh[--oldEndIdx];

}

// 1. 头头比较

else if (sameVNode(oldStartVNode, newStartVNode)) {

patchVNode(oldStartVNode, newStartVNode);

oldStartVNode = oldCh[++oldStartIdx];

newStartVNode = newCh[++newStartIdx];

}

// 2. 尾尾比较

else if (sameVNode(oldEndVNode, newEndVNode)) {

patchVNode(oldEndVNode, newEndVNode);

oldEndVNode = oldCh[--oldEndIdx];

newEndVNode = newCh[--newEndIdx];

}

// 3. 头尾比较

else if (sameVNode(oldStartVNode, newEndVNode)) {

patchVNode(oldStartVNode, newEndVNode);

// 移动节点

nodeOps.insertBefore(parentElm, oldStartVNode.elm, nodeOps.nextSibling(oldEndVNode.elm));

oldStartVNode = oldCh[++oldStartIdx];

newEndVNode = newCh[--newEndIdx];

}

// 4. 尾头比较

else if (sameVNode(oldEndVNode, newStartVNode)) {

patchVNode(oldEndVNode, newStartVNode);

// 移动节点

nodeOps.insertBefore(parentElm, oldEndVNode.elm, oldStartVNode.elm);

oldEndVNode = oldCh[--oldEndIdx];

newStartVNode = newCh[++newStartIdx];

}

// 5. 使用key查找

else {

const idxInOld = findIdxInOld(newStartVNode, oldCh, oldStartIdx, oldEndIdx);

if (!idxInOld) {

// 新节点,创建

createElm(newStartVNode);

} else {

// 找到相同key的节点,移动

const vnodeToMove = oldCh[idxInOld];

patchVNode(vnodeToMove, newStartVNode);

oldCh[idxInOld] = undefined;

nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVNode.elm);

}

newStartVNode = newCh[++newStartIdx];

}

}

// 处理剩余节点

if (oldStartIdx > oldEndIdx) {

// 新增节点

addVNodes(newCh, newStartIdx, newEndIdx);

} else if (newStartIdx > newEndIdx) {

// 删除节点

removeVNodes(oldCh, oldStartIdx, oldEndIdx);

}

}

function sameVNode(a, b) {

return (

a.key === b.key &&

a.tag === b.tag &&

a.isComment === b.isComment &&

isDef(a.data) === isDef(b.data) &&

sameInputType(a, b)

);

}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384双端比较示例:

旧节点:A B C D

新节点:D A B C

第1轮:

头头比较:A !== D ❌

尾尾比较:D === D ✅ → 复用D

旧节点:A B C [D已处理]

新节点:[D已处理] A B C

第2轮:

头头比较:A === A ✅ → 复用A

旧节点:[A已处理] B C

新节点:[A已处理] B C

第3轮:

头头比较:B === B ✅ → 复用B

第4轮:

头头比较:C === C ✅ → 复用C

结果:只移动了D,其他节点复用1234567891011121314151617181920212223Vue3的diff算法(最长递增子序列) ​Vue3采用了更先进的算法,基于最长递增子序列(LIS):

javascript// Vue3 patchKeyedChildren核心逻辑

function patchKeyedChildren(c1, c2, container) {

let i = 0;

const l2 = c2.length;

let e1 = c1.length - 1; // 旧列表尾部索引

let e2 = l2 - 1; // 新列表尾部索引

// 1. 从头开始比较相同的节点

while (i <= e1 && i <= e2) {

const n1 = c1[i], n2 = c2[i];

if (isSameVNodeType(n1, n2)) {

patch(n1, n2, container);

} else {

break;

}

i++;

}

// 2. 从尾开始比较相同的节点

while (i <= e1 && i <= e2) {

const n1 = c1[e1], n2 = c2[e2];

if (isSameVNodeType(n1, n2)) {

patch(n1, n2, container);

} else {

break;

}

e1--;

e2--;

}

// 3. 处理新增节点

if (i > e1) {

if (i <= e2) {

// 有新节点需要挂载

while (i <= e2) {

patch(null, c2[i], container);

i++;

}

}

}

// 4. 处理删除节点

else if (i > e2) {

while (i <= e1) {

unmount(c1[i]);

i++;

}

}

// 5. 处理复杂情况:移动、新增、删除混合

else {

// 使用最长递增子序列算法

const s1 = i, s2 = i;

const keyToNewIndexMap = new Map();

// 建立新节点的key -> index映射

for (i = s2; i <= e2; i++) {

keyToNewIndexMap.set(c2[i].key, i);

}

// 计算最长递增子序列

const newIndexToOldIndexMap = new Array(e2 - s2 + 1).fill(0);

let moved = false;

let maxNewIndexSoFar = 0;

for (i = s1; i <= e1; i++) {

const prevChild = c1[i];

const newIndex = keyToNewIndexMap.get(prevChild.key);

if (newIndex === undefined) {

// 旧节点在新列表中不存在,删除

unmount(prevChild);

} else {

newIndexToOldIndexMap[newIndex - s2] = i + 1;

if (newIndex >= maxNewIndexSoFar) {

maxNewIndexSoFar = newIndex;

} else {

moved = true;

}

patch(prevChild, c2[newIndex], container);

}

}

// 生成最长递增子序列

const increasingNewIndexSequence = moved

? getSequence(newIndexToOldIndexMap)

: [];

// 从后往前遍历,移动和挂载节点

let j = increasingNewIndexSequence.length - 1;

for (i = e2 - s2; i >= 0; i--) {

const nextIndex = s2 + i;

const nextChild = c2[nextIndex];

if (newIndexToOldIndexMap[i] === 0) {

// 新节点,挂载

patch(null, nextChild, container);

} else if (moved) {

if (j < 0 || i !== increasingNewIndexSequence[j]) {

// 需要移动

move(nextChild, container);

} else {

j--;

}

}

}

}

}

// 最长递增子序列算法

function getSequence(arr) {

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[result.length - 1];

// 回溯构建最长递增子序列

while (u-- > 0) {

result[u] = v;

v = p[v];

}

return result;

}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152最长递增子序列示例:

旧节点:[A, B, C, D, E] 索引:[0, 1, 2, 3, 4]

新节点:[A, C, B, E, D] 索引:[0, 2, 1, 4, 3]

1. 预处理:A相同,跳过

2. 中间部分:[B, C, D, E] -> [C, B, E, D]

3. 索引映射:[2, 1, 4, 3]

4. 最长递增子序列:[1, 4] 对应 [B, E]

5. 结果:只需要移动C和D,B和E保持不动

Vue2可能移动:B, C, D

Vue3只移动:C, D

减少了33%的移动操作123456789101112Vue2 vs Vue3 diff算法对比 ​特性Vue2Vue3算法策略双端比较最长递增子序列时间复杂度O(n)O(n log n)空间复杂度O(1)O(n)移动次数可能较多最少移动适用场景简单列表变化复杂列表重排性能表现一般场景好复杂场景更优编译优化无静态标记、静态提升具体优化点 ​1. 预处理优化

javascript// Vue3会先处理头尾相同的节点,减少需要diff的节点数量

// 旧: [A, B, C, D, E]

// 新: [A, B, X, Y, E]

// 预处理后只需要diff: [C, D] vs [X, Y]123452. 静态标记优化

javascript// Vue3编译时会标记静态节点

const _hoisted_1 = createVNode("p", null, "Static text");

function render() {

return createVNode("div", null, [

_hoisted_1, // 静态节点,不参与diff

createVNode("p", null, _ctx.dynamic, 1 /* TEXT */) // 动态节点,参与diff

]);

}1234567893. 最长递增子序列优化

javascript// 例子:旧 [A, B, C, D] -> 新 [A, C, B, D]

// Vue2: 可能移动B和C

// Vue3: 通过LIS算法,只移动B,C保持不动123🧠 深度理解 ​key的作用详解 ​1. 没有key的问题 ​vue

1234567891011121314151617181920212223242526272829问题:

删除Bob后,Charlie的input会显示Bob的值因为Vue就地复用了DOM元素input的值没有绑定到数据,导致错乱2. 使用key解决 ​vue123456789效果:

Vue根据key识别每个元素删除Bob后,Charlie的元素保持不变不会出现数据错乱3. key的最佳实践 ​vue1234567891011121314151617181920为什么Vue3要改进diff算法? ​Vue2双端比较的局限性 ​javascript// 场景:列表项大量重排

// 旧: [1, 2, 3, 4, 5, 6, 7, 8, 9]

// 新: [9, 1, 8, 2, 7, 3, 6, 4, 5]

// Vue2双端比较会产生很多不必要的移动操作

// 因为它无法识别出最优的移动策略123456Vue3最长递增子序列的优势 ​javascript// 同样的场景

// Vue3通过LIS算法找出: [1, 2, 3, 4, 5] 这个递增序列

// 只需要移动 [9, 8, 7, 6],而 [1, 2, 3, 4, 5] 保持不动

// 大大减少了DOM操作次数1234实际性能对比 ​javascript// 测试场景:1000个节点的复杂重排

const oldList = Array.from({length: 1000}, (_, i) => i);

const newList = oldList.slice().sort(() => Math.random() - 0.5);

// Vue2: 平均需要移动 ~500 次

// Vue3: 平均需要移动 ~200 次

// 性能提升约 60%1234567常见误区 ​误区:key可以提高性能

vue

{{ item }}

123456789101112误区:使用index作为key

vue

12345678910111213误区:所有列表都需要key

vue

{{ item }}

{{ item }}

123456789💡 面试回答技巧 ​推荐回答顺序 ​先说diff算法的作用:对比虚拟DOM差异,最小化DOM操作介绍Vue2双端比较:四种比较情况,适合简单场景介绍Vue3 LIS算法:最长递增子序列,减少移动次数说明key的作用:唯一标识节点,避免就地复用问题对比性能差异:复杂场景下Vue3性能更优补充编译时优化:静态标记等优化手段🎯 一句话回答(快速版) ​Vue的diff算法通过同层比较和key标识,将传统O(n³)复杂度优化到O(n)。Vue2用双端比较,Vue3用最长递增子序列算法,在复杂列表重排场景下移动次数更少,性能更好。

📣 口语化回答(推荐) ​面试时可以这样回答:

"Vue的diff算法是虚拟DOM的核心,用来高效对比新旧虚拟DOM的差异。当数据变化时,Vue会生成新的虚拟DOM,然后和旧的虚拟DOM进行对比,找出需要更新的部分,最小化真实DOM操作。

传统的diff算法是O(n³)的复杂度,Vue做了优化,降到了O(n)。主要是三个策略:

第一,只比较同层节点,不跨层级比较。如果父节点不同,直接替换整个子树。

第二,不同类型的节点直接替换,不会去比较内容。比如div变成span,直接删掉div创建span。

第三,用key来标识节点。这样在列表更新时,Vue能知道哪些节点是移动了,哪些是新增或删除的。

Vue2使用双端比较算法,就是同时从新旧节点列表的头尾四个位置开始比较,包括头头比较、尾尾比较、头尾比较、尾头比较四种情况。这种算法对于简单的增删改操作效率很高。

Vue3改用了最长递增子序列算法,这是一个更先进的算法。它会先处理头尾相同的节点,然后对于中间复杂的部分,通过最长递增子序列找出哪些节点可以保持不动,只移动必须移动的节点。

主要区别是:Vue2可能会产生不必要的移动操作,而Vue3通过LIS算法能找到最优的移动策略,大大减少DOM操作次数。特别是在复杂的列表重排场景下,Vue3的性能提升非常明显。

说到key,它的作用是帮助Vue正确识别节点。如果不加key或者用index作为key,Vue会采用"就地复用"策略,可能导致一些奇怪的bug,比如输入框内容错乱。所以最佳实践是用唯一ID作为key。"

重点强调 ​✅ diff算法是同层比较,时间复杂度O(n)✅ key用于正确识别节点,不是提高性能✅ Vue3的优化:静态标记、最长递增子序列✅ 不要使用index作为key✅ Vue3在复杂场景下性能显著优于Vue2可能的追问 ​Q1: 为什么diff算法是O(n)而不是O(n³)?

A: 传统diff算法是O(n³)(树的编辑距离),Vue做了三个假设优化:

只比较同层节点,不跨层级不同类型的节点直接替换使用key识别节点 这样时间复杂度降到O(n)Q2: 什么时候不需要key?

A:

静态列表(不会改变)简单的文本列表(无状态)列表只用于展示(不涉及交互)Q3: key相同但内容不同会怎样?

A: Vue会认为是同一个元素,只更新内容,复用DOM节点。

Q4: Vue3的diff算法有什么缺点吗?

A: 有一些权衡:

空间复杂度增加:需要额外的数组存储映射关系算法复杂度:LIS算法本身比双端比较复杂小列表性能:对于很小的列表,Vue2可能更快但总体来说,Vue3的优化是值得的,特别是在现代应用的复杂场景下。

Q5: 如何优化列表渲染性能?

A: 几个关键点:

正确使用key:使用稳定唯一的key避免使用index作为key:特别是在会重排的列表中合理使用v-show/v-if:根据场景选择虚拟滚动:大列表使用虚拟滚动分页或懒加载:避免一次渲染过多节点加分项 ​提到Vue3的编译时优化(静态提升、补丁标记)说明虚拟DOM的整体架构提到React的diff算法对比结合实际项目经验,说明性能优化的收益提到Vue3的其他性能优化(Proxy、Tree-shaking等)💻 代码示例 ​完整的diff算法演示 ​查看完整代码(包含所有算法实现和测试用例):vue-diff-algorithm.js

简化的diff算法实现 ​javascript// 简化的diff算法实现

class VNode {

constructor(tag, props, children) {

this.tag = tag;

this.props = props;

this.children = children;

this.key = props?.key;

}

}

function patch(oldVNode, newVNode, container) {

// 1. 旧节点不存在,直接挂载

if (!oldVNode) {

mount(newVNode, container);

return;

}

// 2. 新节点不存在,卸载旧节点

if (!newVNode) {

unmount(oldVNode);

return;

}

// 3. 节点类型不同,替换

if (oldVNode.tag !== newVNode.tag) {

unmount(oldVNode);

mount(newVNode, container);

return;

}

// 4. 节点类型相同,更新

const el = (newVNode.el = oldVNode.el);

// 更新props

patchProps(el, oldVNode.props, newVNode.props);

// 更新children(核心diff算法)

patchChildren(oldVNode, newVNode, el);

}123456789101112131415161718192021222324252627282930313233343536373839🔗 相关知识点 ​Vue2和Vue3的区别 - 整体对比v-if和v-show的区别 - 条件渲染Vue生命周期 - 组件生命周期📚 参考资料 ​Vue3源码 - patchKeyedChildrenVue虚拟DOM最长递增子序列算法Vue3性能优化详解