1528 字
8 分钟
Vue3 diff 算法

题目描述#

实现 Vue3 的核心 Diff 算法,用于高效更新虚拟 DOM:

  1. 处理节点类型变化(元素/组件)
  2. 实现双端预处理(前序/后序相同节点)
  3. 构建 key 到位置的映射表
  4. 查找最长递增子序列(LIS)
  5. 基于 LIS 最小化 DOM 操作
  6. 支持节点移动、新增、删除操作 要求完整实现虚拟 DOM 对比更新逻辑,并通过测试用例验证。

解题思路#

  1. 双端比对:从两端开始比对相同的前置/后置节点
  2. 建立索引:构建新节点 key 到索引的映射表
  3. 确定最长序列:找出无需移动的最长递增子序列
  4. 最小化操作:仅移动非 LIS 节点,减少 DOM 操作
  5. 处理特殊情况:新增节点、删除节点、节点类型变化
  6. 高效复用:通过 key 匹配复用 DOM 节点

关键洞察#

  1. 双端预处理:跳过前后相同节点,缩小对比范围
  2. 递增子序列:LIS 确定最小移动操作次数的关键
  3. 原地复用:通过 key 匹配复用现有 DOM 节点
  4. 位运算优化:使用索引映射提高查找效率
  5. 批处理操作:收集变更后统一执行 DOM 操作
  6. 动态规划: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' } },
]
// 创建初始DOM
oldVNodes.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

业务场景#

  1. 组件更新:Vue组件状态变化时的 DOM 高效更新
  2. 列表渲染:动态列表的顺序变化优化(如拖拽排序)
  3. 动画过渡:元素位置变化时的平滑过渡效果
  4. 大型列表:海量数据列表的高性能渲染
  5. 动态表单:字段顺序动态调整时的 DOM 复用

相似题目#

  • 实现 React Diff 算法(困难) 核心:基于 Fiber 架构的双缓冲技术.
  • 虚拟 DOM 实现(中等) 核心:虚拟节点描述 + DOM 创建/更新
  • 最长递增子序列算法(中等) 核心:动态规划/贪心+二分优化实现
  • DOM 批量更新系统(中等) 核心:变更收集 + 统一提交
  • 实现前端路由系统(中等) 核心:History API + 组件切换
Vue3 diff 算法
https://website-truelovings-projects.vercel.app/posts/code/vue/vue3-diff-算法/
作者
欢迎来到StarSky的网站!
发布于
2025-08-15
许可协议
CC BY-NC-SA 4.0