Vue Diff算法原理 发表于 2026年03月11日 更新于 2026年03月11日
vue Vue Diff算法原理 OHNII 2026年03月11日 2026年03月11日 Vue Diff算法原理 核心流程(记忆关键) 记忆口诀:同层比较,深度优先,双端对比,最长递增
核心策略:只比同层,tag+key 判断,Vue2 双端,Vue3 最长递增子序列
1. 基础概念 什么是 Diff 算法?
Diff 算法是一种对比算法,用于找出新旧虚拟 DOM 树的差异
目标是用最小的操作次数将旧 DOM 更新为新 DOM
Vue 的 Diff 算法时间复杂度为 O(n)
为什么需要 Diff 算法? 1 2 3 4 5 6 7 oldDOM.parentNode .replaceChild (newDOM, oldDOM) patch (oldVNode, newVNode)
传统 Diff 的问题 1 2 3 4 5 6 7 传统树的 Diff 算法: - 时间复杂度:O(n³) - 过程: 1. 遍历旧树的每个节点:O(n) 2. 遍历新树的每个节点:O(n) 3. 计算最小编辑距离:O(n) - 结果:性能太差,不适合前端
2. Vue Diff 的核心策略 策略1:同层比较 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 div div / \ / \ p span → p div | span
为什么只比同层?
跨层级移动节点的情况极少(< 1%)
同层比较将 O(n³) 降为 O(n)
性能提升远大于精确度损失
策略2:节点复用判断 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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) ) } <div key="1" >A</div> → <div key="1">B</ div> <div key ="1" > A</div > → <div key="2" >A</div> / / ✗ key 不同<div>A</div> → <span>A</ span>
key 的重要性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div v-for ="(item, index) in list" :key="index" > {{ item.name }} </div> <div v-for ="item in list" :key ="item.id" > {{ item.name }} </div >
3. Vue 2 的 Diff 算法(双端比较) 核心思想
新旧子节点数组各设置头尾两个指针(共 4 个指针)
按照”头头、尾尾、头尾、尾头”的顺序进行比较
找到可复用的节点后移动指针,直到某一方遍历完
详细流程 初始状态:
1 2 3 4 5 6 7 oldStartIdx = 0 oldEndIdx = 3 newStartIdx = 0 newEndIdx = 3
比较过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 function updateChildren (parentElm, oldCh, newCh ) { let oldStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0 ] let oldEndVnode = oldCh[oldEndIdx] let newStartIdx = 0 let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0 ] let newEndVnode = newCh[newEndIdx] while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef (oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] } else if (isUndef (oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode (oldStartVnode, newStartVnode)) { patchVnode (oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode (oldEndVnode, newEndVnode)) { patchVnode (oldEndVnode, newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode (oldStartVnode, newEndVnode)) { patchVnode (oldStartVnode, newEndVnode) nodeOps.insertBefore (parentElm, oldStartVnode.elm , nodeOps.nextSibling (oldEndVnode.elm )) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode (oldEndVnode, newStartVnode)) { patchVnode (oldEndVnode, newStartVnode) nodeOps.insertBefore (parentElm, oldEndVnode.elm , oldStartVnode.elm ) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef (oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx (oldCh, oldStartIdx, oldEndIdx) } idxInOld = isDef (newStartVnode.key ) ? oldKeyToIdx[newStartVnode.key ] : findIdxInOld (newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef (idxInOld)) { createElm (newStartVnode, parentElm, oldStartVnode.elm ) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode (vnodeToMove, newStartVnode)) { patchVnode (vnodeToMove, newStartVnode) oldCh[idxInOld] = undefined nodeOps.insertBefore (parentElm, vnodeToMove.elm , oldStartVnode.elm ) } else { createElm (newStartVnode, parentElm, oldStartVnode.elm ) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { addVnodes (parentElm, newCh, newStartIdx, newEndIdx) } else if (newStartIdx > newEndIdx) { removeVnodes (oldCh, oldStartIdx, oldEndIdx) } }
示例演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
4. Vue 3 的 Diff 算法(快速 Diff) 核心改进
预处理:处理前置和后置的相同节点(掐头去尾)
最长递增子序列(LIS):计算最少移动次数
性能更优:减少不必要的 DOM 移动
详细流程 第 1 步:处理前置相同节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 let i = 0 const l2 = newChildren.length let e1 = oldChildren.length - 1 let e2 = l2 - 1 while (i <= e1 && i <= e2) { const n1 = oldChildren[i] const n2 = newChildren[i] if (sameVnode (n1, n2)) { patch (n1, n2) i++ } else { break } }
第 2 步:处理后置相同节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 while (i <= e1 && i <= e2) { const n1 = oldChildren[e1] const n2 = newChildren[e2] if (sameVnode (n1, n2)) { patch (n1, n2) e1-- e2-- } else { break } }
第 3 步:处理中间乱序部分
情况 1:旧节点遍历完,新节点还有剩余(新增)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (i > e1) { if (i <= e2) { while (i <= e2) { patch (null , newChildren[i], container, anchor) i++ } } }
情况 2:新节点遍历完,旧节点还有剩余(删除)
1 2 3 4 5 6 7 8 9 10 11 12 else if (i > e2) { while (i <= e1) { unmount (oldChildren[i]) i++ } }
情况 3:中间乱序部分(最复杂)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 const keyToNewIndexMap = new Map ()for (let i = s2; i <= e2; i++) { keyToNewIndexMap.set (newChildren[i].key , i) } const newIndexToOldIndexMap = new Array (e2 - s2 + 1 ).fill (0 )let moved = false let maxNewIndexSoFar = 0 for (let i = s1; i <= e1; i++) { const prevChild = oldChildren[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, newChildren[newIndex]) } } const increasingNewIndexSequence = moved ? getSequence (newIndexToOldIndexMap) : [] let j = increasingNewIndexSequence.length - 1 for (let i = e2 - s2; i >= 0 ; i--) { const nextIndex = s2 + i const nextChild = newChildren[nextIndex] const anchor = nextIndex + 1 < l2 ? newChildren[nextIndex + 1 ].el : null if (newIndexToOldIndexMap[i] === 0 ) { patch (null , nextChild, container, anchor) } else if (moved) { if (j < 0 || i !== increasingNewIndexSequence[j]) { move (nextChild, container, anchor) } else { j-- } } }
最长递增子序列算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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[u - 1 ] while (u-- > 0 ) { result[u] = v v = p[v] } return result } getSequence ([5 , 3 , 4 , 0 ])
5. Vue 2 vs Vue 3 Diff 对比 性能对比
算法对比表
特性
Vue 2 双端比较
Vue 3 快速 Diff
核心策略
头尾双指针
前后预处理 + LIS
时间复杂度
O(n)
O(n log n)
移动次数
较多
最少
适用场景
通用
复杂列表
代码复杂度
中等
较高
性能
好
更好
6. 经典面试题解析 题目1:Vue 的 Diff 算法是什么? 答案:
1 2 3 4 5 6 7 8 9 Diff 算法是 Vue 用来对比新旧虚拟 DOM 树,找出差异并最小化 DOM 操作的算法。 核心策略: 1. 同层比较:只比较同一层级的节点,不跨层级 2. 节点复用:通过 tag 和 key 判断是否是同一节点 3. 双端比较(Vue 2):头尾双指针,四种比较方式 4. 快速 Diff(Vue 3):前后预处理 + 最长递增子序列 时间复杂度:O(n)
题目2:为什么 v-for 要加 key? 答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1. 提高 Diff 效率:精确找到可复用的节点2. 避免就地更新:防止状态错乱(input 输入值)3. 减少 DOM 操作:最大化节点复用
题目3:Vue 2 和 Vue 3 的 Diff 有什么区别? 答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Vue 2(双端比较): - 头尾双指针,四种比较方式 - 适合大部分场景 - 时间复杂度 O(n) Vue 3(快速 Diff): - 前后预处理,减少比较范围 - 使用最长递增子序列算法 - 移动次数最少 - 时间复杂度 O(n log n) - 性能更优 核心区别: Vue 3 通过 LIS 算法计算出不需要移动的节点序列, 只移动不在序列中的节点,实现最少移动次数。
7. 面试口述版本 面试官:请解释 Vue 的 Diff 算法
回答框架:
“Vue 的 Diff 算法是用来对比新旧虚拟 DOM 树,找出最小差异并高效更新真实 DOM 的算法。
核心策略有三个:
第一是同层比较 Vue 只比较同一层级的节点,不会跨层级比较。虽然这样会错过一些跨层级移动的优化机会,但这种情况在实际开发中极少出现,而同层比较可以将传统树 Diff 的 O(n³) 复杂度降到 O(n),性能提升非常显著。
第二是节点复用判断 Vue 通过 tag(标签名)和 key 来判断两个节点是否是同一个节点。如果是同一个节点,就会复用 DOM 元素,只更新属性和内容;如果不是,就会删除旧节点,创建新节点。这就是为什么 v-for 必须加 key 的原因,key 可以帮助 Vue 精确识别哪些节点可以复用。
第三是子节点的比较策略 这是 Vue 2 和 Vue 3 的主要区别。
Vue 2 使用双端比较算法,在新旧子节点数组的头尾各设置一个指针,按照头头、尾尾、头尾、尾头的顺序进行比较,找到可复用的节点后移动指针。如果四种方式都没匹配上,就通过 key 建立映射表来查找。
Vue 3 使用快速 Diff 算法,首先会处理前置和后置的相同节点,缩小比较范围。对于中间的乱序部分,Vue 3 引入了最长递增子序列算法,计算出一个不需要移动的节点序列,只对不在这个序列中的节点进行移动或新增。这样可以实现最少的 DOM 移动次数,性能更优。
总结来说 Vue 的 Diff 算法通过同层比较、节点复用和智能的子节点比较策略,实现了高效的虚拟 DOM 更新,这也是 Vue 性能优秀的核心原因之一。”
8. 高频追问 Q1: 为什么不能用 index 作为 key? 答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <div v-for ="(item, index) in list" :key="index" > <input v-model ="item.value" > {{ item.name }} </div >
Q2: key 可以用随机数吗? 答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div v-for ="item in list" :key="Math.random()" > {{ item.name }} </div> <div v-for ="item in list" :key ="item.id" > {{ item.name }} </div >
Q3: 什么情况下可以不用 key? 答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div v-for ="item in staticList" > {{ item.name }} </div> <div v-for ="item in list" > <span > {{ item.name }}</span > </div > <div v-for ="item in list" > {{ item.name }} </div >
Q4: Diff 算法的时间复杂度是多少? 答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 传统树 Diff:O(n³) - 遍历旧树:O(n) - 遍历新树:O(n) - 计算编辑距离:O(n) Vue 2 Diff:O(n) - 同层比较 - 双端比较 Vue 3 Diff:O(n log n) - 同层比较 - 前后预处理:O(n) - 最长递增子序列:O(n log n) 实际性能: 虽然 Vue 3 的理论复杂度更高, 但由于减少了 DOM 移动次数, 实际性能反而更好。
9. 实战技巧 技巧1:合理使用 key 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <div v-for ="item in list" :key="item.id" > {{ item.name }} </div> <div v-for ="(item, index) in list" :key ="index" > {{ item.name }} </div > <div v-for ="item in list" :key ="Math.random()" > {{ item.name }} </div > <div v-for ="item in list" :key ="item" > {{ item.name }} </div >
技巧2:优化列表渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div v-for ="item in list" :key="item.id" v-show="item.visible" > {{ item.name }} </div> computed : { filteredList ( ) { return this .list .filter (item => item.visible ) } } <div v-for ="item in filteredList" :key="item.id" > {{ item.name }} </div> <virtual-list :items ="list" :item-height ="50" > <template #default ="{ item }" > <div > {{ item.name }}</div > </template > </virtual-list >
技巧3:避免不必要的 Diff 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <div v-once> <h1 > {{ title }}</h1 > <p > 这些内容不会更新</p > </div> <div v-memo ="[value1, value2]" > </div > data ( ) { return { list : Object .freeze ([ { id : 1 , name : 'A' }, { id : 2 , name : 'B' } ]) } }
10. 记忆路线图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Diff 算法 ├── 目标:找出新旧虚拟 DOM 的最小差异 ├── 时间复杂度:O(n) │ ├── 核心策略 │ ├── 同层比较:不跨层级 │ ├── 节点复用:tag + key 判断 │ └── 子节点比较:双端 / 快速 Diff │ ├── Vue 2(双端比较) │ ├── 头尾双指针 │ ├── 四种比较方式 │ └── key 映射表 │ ├── Vue 3(快速 Diff) │ ├── 前后预处理 │ ├── 最长递增子序列 │ └── 最少移动次数 │ └── 实战应用 ├── 合理使用 key ├── 优化列表渲染 └── 避免不必要的 Diff
11. 总结 核心要点:
Diff 算法用于对比新旧虚拟 DOM,最小化 DOM 操作
核心策略:同层比较、节点复用(tag + key)
Vue 2 使用双端比较,Vue 3 使用快速 Diff + LIS
key 必须唯一且稳定,不能用 index 或随机数
Vue 3 的 Diff 性能更优,移动次数最少
记忆口诀:
同层比较,深度优先,tag+key 判断
Vue2 双端,Vue3 最长递增
key 要唯一,index 不可取
减少移动,提升性能