2026/1/16 4:50:37
网站建设
项目流程
宁波免费建站,seo基础视频教程,wordpress theme 删除,昆山做网站找文博目录
1. Vue2 Diff 算法原理#xff08;双端比较#xff09;
1.1 核心思想
1.2 源码位置
1.3 sameVnode 判断逻辑
1.4 双端比较流程图解
初始状态
五种比较策略
1.5 完整执行流程演示
1.6 Key 映射查找流程#xff08;第 5 种情况#xff09;
2. Vue3 Diff 算法原…目录1. Vue2 Diff 算法原理双端比较1.1 核心思想1.2 源码位置1.3 sameVnode 判断逻辑1.4 双端比较流程图解初始状态五种比较策略1.5 完整执行流程演示1.6 Key 映射查找流程第 5 种情况2. Vue3 Diff 算法原理快速 Diff LIS2.1 核心思想2.2 源码位置2.3 算法流程图解2.4 完整执行流程演示2.5 LIS 算法核心代码3. Vue2 中 Key 的工作机制3.1 不设置 Key3.2 设置 Key3.3 为什么不能用 index 作为 Key4. Vue3 中 Key 的工作机制4.1 不设置 KeypatchUnkeyedChildren4.2 设置 KeypatchKeyedChildren4.3 Vue3 的 PatchFlags 优化5. 优化原因与 Key 的重要性5.1 算法优化对比5.2 为什么要设置 Key5.3 Key 的最佳实践6. 面试回答策略6.1 简洁版1-2 分钟6.2 详细版3-5 分钟6.3 加分回答追问为什么 Vue3 要用 LISVue3 用 LIS 是为了最小化 DOM 移动次数。1. Vue2 Diff 算法原理双端比较1.1 核心思想Vue2 采用双端比较算法通过四个指针从新旧节点数组的两端向中间靠拢优先处理常见的 DOM 操作场景头部插入、尾部追加、列表反转等。1.2 源码位置src/core/vdom/patch.js ├── patch() // 入口函数 ├── patchVnode() // 更新单个节点 ├── updateChildren() // 核心 Diff 逻辑 ⭐ ├── sameVnode() // 判断是否同一节点 └── createKeyToOldIdx() // 创建 key 映射表1.3 sameVnode 判断逻辑function sameVnode(a, b) { return ( a.key b.key // key 相同最重要 a.tag b.tag // 标签相同 a.isComment b.isComment // 都是/不是注释节点 isDef(a.data) isDef(b.data) // 都有/没有 data sameInputType(a, b) // input 类型相同 ) }关键点key是判断节点是否可复用的第一要素1.4 双端比较流程图解初始状态旧节点: [A, B, C, D] ↑ ↑ oldStart oldEnd 新节点: [D, A, B, C] ↑ ↑ newStart newEnd五种比较策略┌─────────────────────────────────────────────────────────────┐ │ 双端比较优先级 │ ├─────────────────────────────────────────────────────────────┤ │ ① oldStart vs newStart (头-头) │ │ ② oldEnd vs newEnd (尾-尾) │ │ ③ oldStart vs newEnd (头-尾) → 节点右移 │ │ ④ oldEnd vs newStart (尾-头) → 节点左移 │ │ ⑤ 都不匹配 → 使用 key 映射表查找 │ └─────────────────────────────────────────────────────────────┘1.5 完整执行流程演示以[A, B, C, D]→[D, A, B, C]为例═══════════════════════════════════════════════════════════════ 第 1 轮比较 ═══════════════════════════════════════════════════════════════ 旧: [A, B, C, D] 新: [D, A, B, C] ↑ ↑ ↑ ↑ os oe ns ne ① A vs D → ✗ 不匹配 ② D vs C → ✗ 不匹配 ③ A vs C → ✗ 不匹配 ④ D vs D → ✓ 匹配(尾-头) 操作: 将旧节点 D 移动到 oldStart(A) 之前 oe--, ns DOM 变化: [D, A, B, C, _] (_ 表示 D 的原位置已空) ═══════════════════════════════════════════════════════════════ 第 2 轮比较 ═══════════════════════════════════════════════════════════════ 旧: [A, B, C, _] 新: [_, A, B, C] ↑ ↑ ↑ ↑ os oe ns ne ① A vs A → ✓ 匹配(头-头) 操作: 原地 patchos, ns ═══════════════════════════════════════════════════════════════ 第 3 轮比较 ═══════════════════════════════════════════════════════════════ 旧: [_, B, C, _] 新: [_, _, B, C] ↑ ↑ ↑ ↑ os oe ns ne ① B vs B → ✓ 匹配(头-头) 操作: 原地 patchos, ns ═══════════════════════════════════════════════════════════════ 第 4 轮比较 ═══════════════════════════════════════════════════════════════ 旧: [_, _, C, _] 新: [_, _, _, C] ↑↑ ↑↑ os,oe ns,ne ① C vs C → ✓ 匹配 操作: 原地 patch循环结束 ═══════════════════════════════════════════════════════════════ 最终结果 ═══════════════════════════════════════════════════════════════ 总计: 1 次 DOM 移动 3 次原地更新1.6 Key 映射查找流程第 5 种情况当四种双端比较都不匹配时// 1. 创建旧节点的 key → index 映射 const oldKeyToIdx createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 结果: { key-A: 0, key-B: 1, key-C: 2, key-D: 3 } // 2. 用新节点的 key 查找 const idxInOld oldKeyToIdx[newStartVnode.key] if (isUndef(idxInOld)) { // 找不到 → 创建新节点 createElm(newStartVnode, parentElm, oldStartVnode.elm) } else { // 找到了 → 移动复用 const vnodeToMove oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode) oldCh[idxInOld] undefined // 标记已处理 // 移动 DOM 到正确位置 insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // key 相同但节点类型不同当作新节点 createElm(newStartVnode, parentElm, oldStartVnode.elm) } }2. Vue3 Diff 算法原理快速 Diff LIS2.1 核心思想Vue3 采用快速 Diff 算法核心优化预处理先同步相同的前缀和后缀最长递增子序列LIS对乱序部分计算最少移动方案2.2 源码位置packages/runtime-core/src/renderer.ts ├── patch() // 入口 ├── patchChildren() // 子节点分发 ├── patchKeyedChildren() // 有 key 的 Diff ⭐ ├── patchUnkeyedChildren() // 无 key 的 Diff └── getSequence() // 计算 LIS ⭐2.3 算法流程图解┌────────────────────────────────────────────────────────────┐ │ Vue3 快速 Diff 流程 │ ├────────────────────────────────────────────────────────────┤ │ │ │ Step 1: 同步前缀 (从左向右) │ │ ───────────────────────────── │ │ 旧: [A, B, C, D, E] │ │ 新: [A, B, X, Y, E] │ │ ↑ ↑ │ │ 相同前缀直接 patch │ │ │ │ Step 2: 同步后缀 (从右向左) │ │ ───────────────────────────── │ │ 旧: [A, B, C, D, E] │ │ 新: [A, B, X, Y, E] │ │ ↑ │ │ 相同后缀直接 patch │ │ │ │ Step 3: 处理中间乱序区 │ │ ───────────────────────────── │ │ 旧中间: [C, D] │ │ 新中间: [X, Y] │ │ → 使用 LIS 算法最小化移动 │ │ │ └────────────────────────────────────────────────────────────┘2.4 完整执行流程演示以[A, B, C, D, E]→[A, D, B, G, E]为例═══════════════════════════════════════════════════════════════ Step 1: 同步前缀 ═══════════════════════════════════════════════════════════════ 旧: [A, B, C, D, E] ↑ 新: [A, D, B, G, E] ↑ A A → patch, i i 1 时B ! D停止前缀同步 ═══════════════════════════════════════════════════════════════ Step 2: 同步后缀 ═══════════════════════════════════════════════════════════════ 旧: [A, B, C, D, E] ↑ 新: [A, D, B, G, E] ↑ E E → patch, e1--, e2-- e1 3, e2 3 时停止 ═══════════════════════════════════════════════════════════════ Step 3: 处理中间乱序区 ═══════════════════════════════════════════════════════════════ 旧中间: [B, C, D] (索引 1-3) 新中间: [D, B, G] (索引 1-3) 3.1 建立新节点 key → index 映射 keyToNewIndexMap { D: 1, B: 2, G: 3 } 3.2 遍历旧中间节点构建 newIndexToOldIndexMap 初始: [0, 0, 0] (0 表示新节点在旧中不存在) 处理旧节点 B (旧索引 1): → 在新中位置 2 → newIndexToOldIndexMap[2-1] 11 2 → [0, 2, 0] 处理旧节点 C (旧索引 2): → 在新中不存在 → 卸载 C 处理旧节点 D (旧索引 3): → 在新中位置 1 → newIndexToOldIndexMap[1-1] 31 4 → [4, 2, 0] 最终: newIndexToOldIndexMap [4, 2, 0] ↑ ↑ ↑ D B G(新) 3.3 计算最长递增子序列 (LIS) 数组 [4, 2, 0] 中非零部分 [4, 2] LIS [1] (索引 1即 B 不需要移动) 3.4 从后向前遍历新中间节点 i 2 (G): newIndexToOldIndexMap[2] 0 → 新节点mount i 1 (B): 在 LIS 中 → 不移动 i 0 (D): 不在 LIS 中 → 移动到正确位置 ═══════════════════════════════════════════════════════════════ 最终结果 ═══════════════════════════════════════════════════════════════ 操作统计: - patch: 2 次 (A, E) - 卸载: 1 次 (C) - 挂载: 1 次 (G) - 移动: 1 次 (D) 相比 Vue2移动次数更少2.5 LIS 算法核心代码// 获取最长递增子序列的索引数组 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 }3. Vue2 中 Key 的工作机制3.1 不设置 Keyul li v-foritem in list{{ item.name }}/li /ul工作方式按索引位置就地复用═══════════════════════════════════════════════════════════════ 示例头部插入新元素 ═══════════════════════════════════════════════════════════════ 数据变化: [B, C] → [A, B, C] 无 Key 时的更新过程: 旧 DOM: [li-0: B] [li-1: C] 新数据: [A, B, C] 更新策略 (按索引复用): li-0: B → A (更新文本) li-1: C → B (更新文本) li-2: 不存在 → 创建 C 实际 DOM 操作: ✗ 2 次文本更新 1 次创建 ═══════════════════════════════════════════════════════════════ 问题状态错乱 ═══════════════════════════════════════════════════════════════ li v-foritem in list {{ item.name }} input typetext !-- 用户可能已输入内容 -- /li 场景: 用户在第一个输入框输入了 Hello 数据: [B, C] → [A, B, C] 无 Key 时: li-0 被复用文本 B→A但 input 状态保留 结果: A 旁边显示 Hello ← 状态错乱 用户输入的是 B 的内容却显示在 A 旁边3.2 设置 Keyul li v-foritem in list :keyitem.id{{ item.name }}/li /ul工作方式通过 Key 精确匹配节点═══════════════════════════════════════════════════════════════ 示例头部插入新元素 ═══════════════════════════════════════════════════════════════ 数据变化: [{ id: 2, name: B }, { id: 3, name: C }] ↓ [{ id: 1, name: A }, { id: 2, name: B }, { id: 3, name: C }] 有 Key 时的更新过程: 旧 DOM: [li(key2): B] [li(key3): C] 新数据: [key1: A, key2: B, key3: C] 更新策略 (按 Key 匹配): key1: 不存在 → 创建新节点插入到最前面 key2: 匹配 li(key2) → 复用无需更新 key3: 匹配 li(key3) → 复用无需更新 实际 DOM 操作: ✓ 1 次创建 1 次插入 性能对比: 无 Key: 2 次更新 1 次创建 3 次操作 有 Key: 1 次创建 1 次插入 2 次操作3.3 为什么不能用 index 作为 Key!-- ❌ 错误示范 -- li v-for(item, index) in list :keyindex═══════════════════════════════════════════════════════════════ index 作为 Key 的问题 ═══════════════════════════════════════════════════════════════ 数据: [A, B, C] → 删除 B → [A, C] 使用 index 作为 key: 旧: [li(key0): A] [li(key1): B] [li(key2): C] 新: [li(key0): A] [li(key1): C] Diff 过程: key0: A → A ✓ 复用 key1: B → C ✗ 文本更新 (本应复用 C 的 DOM) key2: C → 不存在删除 结果: 删除了 C 的 DOM更新了 B 的文本 如果 B 有状态状态会错误地保留给 C 使用 item.id 作为 key: 旧: [li(keya): A] [li(keyb): B] [li(keyc): C] 新: [li(keya): A] [li(keyc): C] Diff 过程: keya: 复用 keyb: 不存在于新列表删除 ✓ keyc: 复用 结果: 正确删除 B 的 DOMC 保持不变4. Vue3 中 Key 的工作机制4.1 不设置 KeypatchUnkeyedChildren// 简化的 patchUnkeyedChildren 逻辑 function patchUnkeyedChildren(c1, c2, container) { const oldLength c1.length const newLength c2.length const commonLength Math.min(oldLength, newLength) // 1. 按索引逐个 patch for (let i 0; i commonLength; i) { patch(c1[i], c2[i], container) } // 2. 新的多 → 挂载 if (newLength oldLength) { mountChildren(c2.slice(commonLength), container) } // 3. 旧的多 → 卸载 if (oldLength newLength) { unmountChildren(c1.slice(commonLength)) } }问题与 Vue2 相同按位置复用导致状态错乱4.2 设置 KeypatchKeyedChildren═══════════════════════════════════════════════════════════════ Vue3 Key 的优势 ═══════════════════════════════════════════════════════════════ 场景: [A, B, C, D] → [A, C, B, D] (B、C 交换位置) Vue2 双端 Diff: 1. A-A 匹配 ✓ 2. D-D 匹配 ✓ 3. B-C 不匹配C-B 不匹配 4. 使用 keyMap 查找移动 B 或 C → 可能需要 1-2 次移动 Vue3 快速 Diff: 1. 前缀: A-A 匹配 ✓ 2. 后缀: D-D 匹配 ✓ 3. 中间: [B, C] → [C, B] - newIndexToOldIndexMap [3, 2] (C 在旧索引 2B 在旧索引 1) - LIS [0] (C 不动) - 只移动 B → 精确 1 次移动 Vue3 通过 LIS 保证移动次数最少4.3 Vue3 的 PatchFlags 优化// 编译时标记 const PatchFlags { TEXT: 1, // 动态文本 CLASS: 2, // 动态 class STYLE: 4, // 动态 style PROPS: 8, // 动态 props FULL_PROPS: 16, // 有动态 key 的 props // ... } // 模板 div :classcls{{ msg }}/div // 编译结果 createVNode(div, { class: _ctx.cls }, _ctx.msg, PatchFlags.TEXT | PatchFlags.CLASS // 标记只需更新文本和 class )运行时根据 PatchFlags 跳过不必要的比较进一步提升性能。5. 优化原因与 Key 的重要性5.1 算法优化对比特性Vue2 双端 DiffVue3 快速 Diff前缀/后缀处理无专门优化先同步缩小比较范围乱序处理双端 keyMapLIS 最少移动时间复杂度O(n)O(n) O(nlogn) LIS移动次数可能非最优理论最少编译优化无PatchFlags 靶向更新5.2 为什么要设置 Key┌─────────────────────────────────────────────────────────────┐ │ Key 的三大作用 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 正确性保障 │ │ ───────────── │ │ • 避免就地复用导致的状态错乱 │ │ • 确保组件实例与数据的正确对应 │ │ • 保持表单输入、动画状态的一致性 │ │ │ │ 2. 性能优化 │ │ ───────────── │ │ • O(1) 查找 vs O(n) 遍历 │ │ • 减少不必要的 DOM 创建/销毁 │ │ • 最大化节点复用 │ │ │ │ 3. 算法效率 │ │ ───────────── │ │ • 启用 Vue3 的 LIS 优化 │ │ • 精确定位需要移动的节点 │ │ • 避免全量更新 │ │ │ └─────────────────────────────────────────────────────────────┘5.3 Key 的最佳实践// ✅ 正确使用唯一业务 ID li v-foruser in users :keyuser.id // ✅ 正确组合生成唯一 Key li v-foritem in items :key${item.type}-${item.id} // ❌ 错误使用 index li v-for(item, index) in items :keyindex // ❌ 错误使用随机数每次渲染都变 li v-foritem in items :keyMath.random() // ⚠️ 注意纯展示列表且永不变动时index 可接受 li v-for(item, index) in staticList :keyindex6. 面试回答策略6.1 简洁版1-2 分钟Diff 算法的目标是以最小 DOM 操作更新视图。Vue2使用双端比较四个指针从两端向中间靠拢优先匹配头头、尾尾、头尾、尾头都不匹配时用 Key 映射表查找。Vue3使用快速 Diff先同步前缀后缀缩小范围对乱序部分用最长递增子序列LIS算法计算最少移动方案配合编译时的 PatchFlags 实现靶向更新。Key 的作用作为节点身份标识确保正确复用、避免状态错乱、提升 Diff 效率。不要用 index 作为 Key因为列表变动时 index 会变化导致错误复用。6.2 详细版3-5 分钟┌─────────────────────────────────────────────────────────────┐ │ 面试回答结构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 开场说明 Diff 的目标 │ │ Diff 算法的核心目标是找出新旧虚拟 DOM 的差异 │ │ 以最小的 DOM 操作完成更新因为 DOM 操作是昂贵的。 │ │ │ │ 2. Vue2 实现 │ │ Vue2 采用双端比较通过四个指针... │ │ (描述五种比较策略) │ │ │ │ 3. Vue3 优化 │ │ Vue3 做了两个关键优化 │ │ 一是先处理相同前缀后缀缩小比较范围 │ │ 二是对乱序部分使用 LIS 算法保证移动次数最少。 │ │ │ │ 4. Key 的作用 │ │ Key 是节点的身份证有三个作用 │ │ 正确性、性能、算法效率... │ │ (举例说明无 Key 的状态错乱问题) │ │ │ │ 5. 实际应用 │ │ 在实际开发中我会用业务唯一 ID 作为 Key │ │ 避免使用 index因为... │ │ │ └─────────────────────────────────────────────────────────────┘6.3 加分回答追问为什么 Vue3 要用 LISLIS 能找出最长的相对顺序不变的节点序列这些节点不需要移动只需移动剩余节点从而保证 DOM 移动次数最少。Vue3 用 LIS 是为了最小化 DOM 移动次数。原理是LIS 找出相对顺序不变的最长节点序列这些节点不需要移动只移动剩余节点。比如[B,C,D]变成[C,D,B]LIS 是[C,D]所以只需移动 B而不是移动 C 和 D。虽然计算 LIS 需要 O(n log n)但 DOM 操作比 JS 计算昂贵得多这个权衡非常值得。