1528 字
8 分钟
Vue3 diff 算法

题目描述
实现 Vue3 的核心 Diff 算法,用于高效更新虚拟 DOM:
- 处理节点类型变化(元素/组件)
- 实现双端预处理(前序/后序相同节点)
- 构建 key 到位置的映射表
- 查找最长递增子序列(LIS)
- 基于 LIS 最小化 DOM 操作
- 支持节点移动、新增、删除操作 要求完整实现虚拟 DOM 对比更新逻辑,并通过测试用例验证。
解题思路
- 双端比对:从两端开始比对相同的前置/后置节点
- 建立索引:构建新节点 key 到索引的映射表
- 确定最长序列:找出无需移动的最长递增子序列
- 最小化操作:仅移动非 LIS 节点,减少 DOM 操作
- 处理特殊情况:新增节点、删除节点、节点类型变化
- 高效复用:通过 key 匹配复用 DOM 节点
关键洞察
- 双端预处理:跳过前后相同节点,缩小对比范围
- 递增子序列:LIS 确定最小移动操作次数的关键
- 原地复用:通过 key 匹配复用现有 DOM 节点
- 位运算优化:使用索引映射提高查找效率
- 批处理操作:收集变更后统一执行 DOM 操作
- 动态规划:LIS 求解使用贪心 + 二分优化
代码流程
flowchart TD A[对比新旧节点] --> B{节点类型相同?} B -->|是| C[更新属性/内容] B -->|否| D[替换节点] C --> E[对比子节点] E --> F[双端预处理] F --> G[建立key映射] G --> H[确定最长递增子序列] H --> I[移动/新增/删除节点]
代码实现
function diff(oldChildren, newChildren, parentEl) { // 1. 双端预处理(跳过相同前置节点) let oldStartIdx = 0 let oldEndIdx = oldChildren.length - 1 let newStartIdx = 0 let newEndIdx = newChildren.length - 1 let oldStartNode = oldChildren[oldStartIdx] let oldEndNode = oldChildren[oldEndIdx] let newStartNode = newChildren[newStartIdx] let newEndNode = newChildren[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (!oldStartNode) { oldStartNode = oldChildren[++oldStartIdx] } else if (!oldEndNode) { oldEndNode = oldChildren[--oldEndIdx] } else if (isSameNode(oldStartNode, newStartNode)) { patchNode(oldStartNode, newStartNode) oldStartNode = oldChildren[++oldStartIdx] newStartNode = newChildren[++newStartIdx] } else if (isSameNode(oldEndNode, newEndNode)) { patchNode(oldEndNode, newEndNode) oldEndNode = oldChildren[--oldEndIdx] newEndNode = newChildren[--newEndIdx] } else { break } }
// 2. 新旧节点完全匹配的情况 if (oldStartIdx > oldEndIdx && newStartIdx <= newEndIdx) { // 新增节点 const anchor = newChildren[newEndIdx + 1]?.el || null for (let i = newStartIdx; i <= newEndIdx; i++) { parentEl.insertBefore(createEl(newChildren[i]), anchor) } return } else if (newStartIdx > newEndIdx && oldStartIdx <= oldEndIdx) { // 删除节点 for (let i = oldStartIdx; i <= oldEndIdx; i++) { parentEl.removeChild(oldChildren[i].el) } return }
// 3. 处理未知序列 const oldStart = oldStartIdx const oldEnd = oldEndIdx const newStart = newStartIdx const newEnd = newEndIdx
// 构建 key 索引映射 const keyIndexMap = {} for (let i = newStart; i <= newEnd; i++) { keyIndexMap[newChildren[i].key] = i }
// 遍历旧节点序列 const toMove = [] const newIndexMap = new Array(oldEnd - oldStart + 1).fill(-1)
for (let i = oldStart; i <= oldEnd; i++) { const oldNode = oldChildren[i] if (!oldNode) continue
const newIndex = keyIndexMap[oldNode.key] if (newIndex === undefined) { // 删除不再存在的节点 parentEl.removeChild(oldNode.el) } else { newIndexMap[i - oldStart] = newIndex toMove.push({ oldIndex: i, newIndex })
if (newIndex < newStart || newIndex > newEnd) { patchNode(oldNode, newChildren[newIndex]) } } }
// 4. 计算最长递增子序列(LIS) const lis = getSequence(newIndexMap.filter(i => i !== -1)) let lisIndex = lis.length - 1
// 5. 执行移动操作 for (let i = toMove.length - 1; i >= 0; i--) { const move = toMove[i] const curIndex = move.oldIndex - oldStart const newIndex = move.newIndex - newStart
if (newIndexMap[curIndex] === -1) { // 新增节点 const anchor = newChildren[newIndex + 1]?.el || null parentEl.insertBefore(createEl(newChildren[newIndex]), anchor) } else { // 移动节点 if (lis[lisIndex] !== i) { const anchor = newChildren[newIndex + 1]?.el || null parentEl.insertBefore(oldChildren[move.oldIndex].el, anchor) } else { lisIndex-- } } }}
// 判断是否为相同节点function isSameNode(a, b) { return a.key === b.key && a.type === b.type}
// 更新节点属性/内容function patchNode(oldNode, newNode) { const el = (newNode.el = oldNode.el)
// 更新属性(简化实现) Object.entries(newNode.props).forEach(([key, value]) => { if (oldNode.props[key] !== value) { el.setAttribute(key, value) } })
// 更新内容(简化实现) if (newNode.children !== oldNode.children) { el.textContent = newNode.children }}
// 创建DOM元素(简化)function createEl(vnode) { const el = document.createElement(vnode.type)
// 设置属性 Object.entries(vnode.props).forEach(([key, value]) => { el.setAttribute(key, value) })
// 设置内容 if (typeof vnode.children === 'string') { el.textContent = vnode.children }
vnode.el = el return el}
// 获取最长递增子序列(贪心+二分)function getSequence(arr) { const p = arr.slice() const result = let i, j, u, v, c
for (i = 0; i < arr.length; i++) { const current = arr[i]
if (current !== -1) { j = result[result.length - 1] if (arr[j] < current) { p[i] = j result.push(i) continue }
u = 0 v = result.length - 1
while (u < v) { c = (u + v) >> 1 if (arr[result[c]] < current) { u = c + 1 } else { v = c } }
if (current < 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}
代码测试
// 创建父元素const container = document.createElement('div')
// 初始子节点const oldVNodes = [ { key: 'a', type: 'div', children: 'A', props: { class: 'item' } }, { key: 'b', type: 'div', children: 'B', props: { class: 'item' } }, { key: 'c', type: 'div', children: 'C', props: { class: 'item' } }, { key: 'd', type: 'div', children: 'D', props: { class: 'item' } },]
// 创建初始DOMoldVNodes.forEach(vnode => { const el = createEl(vnode) container.appendChild(el)})
// 新子节点(顺序变化 + 新增)const newVNodes = [ { key: 'd', type: 'div', children: 'D updated', props: { class: 'item updated' } }, { key: 'a', type: 'div', children: 'A', props: { class: 'item' } }, { key: 'c', type: 'div', children: 'C', props: { class: 'item' } }, { key: 'e', type: 'div', children: 'E new', props: { class: 'item new' } },]
// 执行Diff算法更新diff(oldVNodes, newVNodes, container)
// 最终DOM顺序:// D updated -> A -> C -> E new
业务场景
- 组件更新:Vue组件状态变化时的 DOM 高效更新
- 列表渲染:动态列表的顺序变化优化(如拖拽排序)
- 动画过渡:元素位置变化时的平滑过渡效果
- 大型列表:海量数据列表的高性能渲染
- 动态表单:字段顺序动态调整时的 DOM 复用
相似题目
- 实现 React Diff 算法(困难) 核心:基于 Fiber 架构的双缓冲技术.
- 虚拟 DOM 实现(中等) 核心:虚拟节点描述 + DOM 创建/更新
- 最长递增子序列算法(中等) 核心:动态规划/贪心+二分优化实现
- DOM 批量更新系统(中等) 核心:变更收集 + 统一提交
- 实现前端路由系统(中等) 核心:History API + 组件切换