vueDiff
Vue 3 虚拟 DOM 的 Diff 算法实现,使用双端比较和最长递增子序列优化。
函数签名
typescript
function vueDiff(oldVNode: VNode, newVNode: VNode): DiffResult
function createVNode(
type: string | symbol,
props?: Record<string, any> | null,
children?: VNode[] | string | null
): VNode
function diffChildrenList(
oldChildren: VNode[],
newChildren: VNode[]
): DiffResult
function formatPatch(patch: Patch): string
function getPatchStats(patches: Patch[]): PatchStats
interface VNode {
type: string | symbol
props?: Record<string, any> | null
children?: VNode[] | string | null
key?: string | number | null
text?: string
}
interface DiffResult {
patches: Patch[]
hasChanges: boolean
}
interface Patch {
type: 'CREATE' | 'REMOVE' | 'REPLACE' | 'UPDATE' | 'REORDER' | 'TEXT'
oldVNode?: VNode
newVNode?: VNode
index?: number
moveToIndex?: number
propsPatches?: {
add?: Record<string, any>
remove?: string[]
update?: Record<string, any>
}
}参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
oldVNode | VNode | 是 | 旧虚拟 DOM 节点 |
newVNode | VNode | 是 | 新虚拟 DOM 节点 |
返回值
| 类型 | 说明 |
|---|---|
DiffResult | Diff 结果,包含 patch 操作列表和变化标志 |
工作原理
- 双端比较:同时从新旧列表的头尾开始比较
- 四种情况:
- 旧头 vs 新头
- 旧尾 vs 新尾
- 旧头 vs 新尾
- 旧尾 vs 新头
- Key 查找:使用 key 快速定位可复用节点
- 最长递增子序列:优化移动操作,减少 DOM 移动次数
- 递归处理:对子节点递归执行 Diff
Patch 操作类型:
- CREATE - 创建新节点
- REMOVE - 删除节点
- REPLACE - 替换节点
- UPDATE - 更新属性
- REORDER - 重新排序(移动)
- TEXT - 更新文本内容
基础示例
typescript
import { vueDiff, createVNode } from 'zcw-shared/functions/utils/vueDiff'
// 创建旧 VNode 树
const oldVNode = createVNode('div', { class: 'container' }, [
createVNode('li', { key: 'a' }, 'Item A'),
createVNode('li', { key: 'b' }, 'Item B'),
createVNode('li', { key: 'c' }, 'Item C')
])
// 创建新 VNode 树
const newVNode = createVNode('div', { class: 'container active' }, [
createVNode('li', { key: 'a' }, 'Item A modified'),
createVNode('li', { key: 'c' }, 'Item C'),
createVNode('li', { key: 'd' }, 'Item D')
])
// 执行 Diff
const result = vueDiff(oldVNode, newVNode)
console.log(result.hasChanges) // true
console.log(result.patches) // Patch 操作列表列表 Diff
typescript
import { diffChildrenList, createVNode } from 'zcw-shared/functions/utils/vueDiff'
const oldList = [
createVNode('li', { key: 1 }, 'A'),
createVNode('li', { key: 2 }, 'B'),
createVNode('li', { key: 3 }, 'C'),
createVNode('li', { key: 4 }, 'D')
]
const newList = [
createVNode('li', { key: 4 }, 'D'),
createVNode('li', { key: 2 }, 'B'),
createVNode('li', { key: 3 }, 'C'),
createVNode('li', { key: 1 }, 'A')
]
const result = diffChildrenList(oldList, newList)
// 查看操作
result.patches.forEach(patch => {
console.log(formatPatch(patch))
})
// REORDER li from 3 to 0
// ...Key 的重要性
typescript
// 不使用 key - 效率低
const oldList = [
createVNode('li', null, 'A'),
createVNode('li', null, 'B'),
createVNode('li', null, 'C')
]
const newList = [
createVNode('li', null, 'C'),
createVNode('li', null, 'A'),
createVNode('li', null, 'B')
]
// 会产生大量 TEXT 更新操作
// 使用 key - 高效
const oldListWithKey = [
createVNode('li', { key: 'a' }, 'A'),
createVNode('li', { key: 'b' }, 'B'),
createVNode('li', { key: 'c' }, 'C')
]
const newListWithKey = [
createVNode('li', { key: 'c' }, 'C'),
createVNode('li', { key: 'a' }, 'A'),
createVNode('li', { key: 'b' }, 'B')
]
// 只会产生 REORDER 操作,不需要更新内容属性 Diff
typescript
const old = createVNode('div', {
class: 'container',
id: 'app',
'data-value': '123'
}, null)
const new_ = createVNode('div', {
class: 'container active',
id: 'app',
style: 'color: red'
}, null)
const result = vueDiff(old, new_)
// 结果包含:
// UPDATE: add style, update class, remove data-value嵌套节点 Diff
typescript
const oldVNode = createVNode('div', null, [
createVNode('header', { key: 'header' }, [
createVNode('h1', null, 'Title')
]),
createVNode('main', { key: 'main' }, [
createVNode('p', null, 'Content')
])
])
const newVNode = createVNode('div', null, [
createVNode('header', { key: 'header' }, [
createVNode('h1', null, 'New Title')
]),
createVNode('main', { key: 'main' }, [
createVNode('p', null, 'Content'),
createVNode('p', null, 'More content')
]),
createVNode('footer', { key: 'footer' }, [
createVNode('span', null, 'Footer')
])
])
const result = vueDiff(oldVNode, newVNode)
// 递归比较所有子节点实际应用场景
虚拟列表优化
typescript
// 计算最少的 DOM 操作
function updateList(oldItems: any[], newItems: any[]) {
const oldVNodes = oldItems.map(item =>
createVNode('li', { key: item.id }, item.name)
)
const newVNodes = newItems.map(item =>
createVNode('li', { key: item.id }, item.name)
)
const result = diffChildrenList(oldVNodes, newVNodes)
// 根据 patch 执行最少的 DOM 操作
result.patches.forEach(patch => {
switch (patch.type) {
case 'CREATE':
// createElement and append
break
case 'REMOVE':
// removeChild
break
case 'REORDER':
// move element
break
}
})
}组件更新优化
typescript
class Component {
private vnode: VNode | null = null
render() {
// 返回新的 VNode
return createVNode('div', this.props, this.children)
}
update() {
const newVNode = this.render()
if (this.vnode) {
const result = vueDiff(this.vnode, newVNode)
if (result.hasChanges) {
// 应用 patches
this.applyPatches(result.patches)
}
}
this.vnode = newVNode
}
}Diff 分析工具
typescript
function analyzeDiff(oldData: any[], newData: any[]) {
const oldVNodes = oldData.map((item, i) =>
createVNode('item', { key: item.id }, JSON.stringify(item))
)
const newVNodes = newData.map((item, i) =>
createVNode('item', { key: item.id }, JSON.stringify(item))
)
const result = diffChildrenList(oldVNodes, newVNodes)
const stats = getPatchStats(result.patches)
console.log('Diff 分析:')
console.log(` 总操作数: ${stats.total}`)
console.log(` 创建: ${stats.create}`)
console.log(` 删除: ${stats.remove}`)
console.log(` 移动: ${stats.reorder}`)
return stats
}性能对比
typescript
// 场景1:顺序调整
const old = ['A', 'B', 'C', 'D'].map((v, i) => createVNode('li', { key: i }, v))
const new_ = ['D', 'B', 'C', 'A'].map((v, i) => createVNode('li', { key: i }, v))
const result1 = diffChildrenList(old, new_)
console.log('移动操作:', result1.patches.filter(p => p.type === 'REORDER').length)
// 场景2:增删改
const result2 = diffChildrenList(
[createVNode('li', { key: 'a' }, 'A')],
[
createVNode('li', { key: 'a' }, 'A modified'),
createVNode('li', { key: 'b' }, 'B')
]
)
console.log('总操作:', result2.patches.length)高效的 Vue Diff 算法,理解虚拟 DOM 的核心原理!