Skip to content

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>
  }
}

参数

参数名类型必填说明
oldVNodeVNode旧虚拟 DOM 节点
newVNodeVNode新虚拟 DOM 节点

返回值

类型说明
DiffResultDiff 结果,包含 patch 操作列表和变化标志

工作原理

  1. 双端比较:同时从新旧列表的头尾开始比较
  2. 四种情况
    • 旧头 vs 新头
    • 旧尾 vs 新尾
    • 旧头 vs 新尾
    • 旧尾 vs 新头
  3. Key 查找:使用 key 快速定位可复用节点
  4. 最长递增子序列:优化移动操作,减少 DOM 移动次数
  5. 递归处理:对子节点递归执行 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 的核心原理!