第5章 虚拟DOM与Diff算法

5.1 虚拟DOM设计哲学

5.1.1 虚拟DOM存在意义

// 真实DOM操作代价示例
const start = Date.now()
for(let i=0; i<1000; i++){
  document.createElement('div') // 真实DOM操作非常耗时
}
console.log('耗时:', Date.now()-start) // 约20-50ms

// 虚拟DOM操作示例
const vnodes = []
for(let i=0; i<100000; i++){ // 10万次操作
  vnodes.push({ tag: 'div', data: {}, children: [] })
}
console.log('JS对象操作耗时极低') // <1ms

核心价值

  1. 跨平台能力:抽象渲染层,可对接不同平台
  2. 批量更新:合并多次DOM操作
  3. 高效更新:通过Diff算法最小化变更

5.1.2 虚拟节点结构

// 完整的VNode结构
{
  tag: 'div',             // 标签名/组件名
  data: {                 // 属性/指令/事件等
    attrs: { id: 'app' },
    on: { click: handler }
  },
  children: [             // 子节点
    { 
      tag: 'span', 
      text: 'Counter: ' + this.count 
    }
  ],
  elm: document.createElement('div'), // 对应的真实DOM
  key: 'unique_key',      // 优化Diff的关键
  context: vm             // 组件上下文
}

关键属性解析

  • tag:标识节点类型(平台标签/组件)
  • data:包含所有节点属性/事件/指令等信息
  • children:子节点树结构
  • key:Diff算法的优化关键
  • elm:与真实DOM的映射关系

5.2 Diff算法核心原理

5.2.1 Diff算法流程图解

新旧VNode对比
是否为相同节点?
精细化比较
整体替换
更新属性
更新子节点
双端对比算法

5.2.2 同级比较策略

算法约束

  1. 只进行同层级比较
  2. 不同节点类型直接替换
  3. 通过key标识可复用的节点

代码实现

function sameVnode(a, b) {
  return (
    a.key === b.key &&      // key相同
    a.tag === b.tag &&      // 标签相同
    a.isComment === b.isComment && // 注释节点
    isDef(a.data) === isDef(b.data) && // 数据定义一致
    sameInputType(a, b)     // 相同的输入类型
  )
}

5.3 子节点更新策略

5.3.1 双端对比算法

四指针遍历策略

function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let newStartIdx = 0
  let newEndIdx = newCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 四种比较情况
    if (sameVnode(oldStartVnode, newStartVnode)) {
      // 情况1:头头相同
      patchVnode(...)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 情况2:尾尾相同
      patchVnode(...)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // 情况3:头尾相同
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
      patchVnode(...)
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // 情况4:尾头相同
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
      patchVnode(...)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // 使用key的查找策略
      const idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (idxInOld) {
        // 移动已有节点
        parentElm.insertBefore(oldCh[idxInOld].elm, oldStartVnode.elm)
      } else {
        // 创建新节点
        createElm(newStartVnode, parentElm, oldStartVnode.elm)
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  
  // 处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    addVnodes(...)
  } else {
    removeVnodes(...)
  }
}

算法优势

  1. 最大程度复用现有节点
  2. 减少DOM操作次数
  3. 时间复杂度优化到O(n)

5.4 核心Diff流程实现

5.4.1 节点更新函数

function patchVnode(oldVnode, newVnode) {
  const elm = newVnode.elm = oldVnode.elm
  const oldCh = oldVnode.children
  const newCh = newVnode.children

  // 属性更新
  if (newVnode.data) {
    updateAttrs(elm, newVnode, oldVnode)
    updateClass(elm, newVnode, oldVnode)
    updateDOMListeners(elm, newVnode, oldVnode)
  }

  // 文本节点
  if (newVnode.text) {
    if (oldVnode.text !== newVnode.text) {
      elm.textContent = newVnode.text
    }
  } 
  // 子节点更新
  else {
    if (oldCh && newCh) {
      updateChildren(elm, oldCh, newCh) // 触发Diff算法
    } else if (newCh) {
      addVnodes(elm, null, newCh, 0, newCh.length-1)
    } else if (oldCh) {
      removeVnodes(elm, oldCh, 0, oldCh.length-1)
    }
  }
}

5.4.2 Key的作用原理

无key时的更新问题


<li>Item 1li>  
<li>Item 2li>


<li>Item 0li>  
<li>Item 1li>
<li>Item 2li>

有key的正确处理


<li key="a">Item 1li>
<li key="b">Item 2li>


<li key="c">Item 0li>  
<li key="a">Item 1li>  
<li key="b">Item 2li>  

Key的最佳实践

  1. 使用唯一稳定的标识(如ID)
  2. 避免使用索引作为key
  3. 相同父元素的子元素必须有唯一key

5.5 性能优化策略

5.5.1 静态节点提升

编译阶段优化

// 原始模板
<div>
  <h1>Static Title</h1>  <!-- 静态节点 -->
  <p>{{ dynamicContent }}</p>
</div>

// 优化后渲染函数
const _hoisted_1 = _c('h1', [_v("Static Title")])

function render() {
  return _c('div', [
    _hoisted_1, // 直接复用静态节点
    _c('p', [_v(_s(dynamicContent))])
  ])
}

5.5.2 差异更新统计

// 性能分析示例
const stats = {
  totalNodes: 0,
  created: 0,
  removed: 0,
  moved: 0
}

function patch() {
  // ...在每次操作时更新统计信息
}

// 输出结果示例
console.log(`节点总数: ${stats.totalNodes}`)
console.log(`新建节点: ${stats.created}`)
console.log(`移动节点: ${stats.moved}`)
console.log(`删除节点: ${stats.removed}`)

优化指标

  1. 减少新建节点数
  2. 减少DOM操作次数
  3. 提高节点复用率

本章重点总结:

  1. 虚拟DOM本质:JS对象描述的DOM结构
  2. Diff算法核心:同级比较与双端指针策略
  3. Key的作用:提高节点复用效率
  4. 性能优化:静态提升与批量更新

深度实践建议

  1. 对比有无key时的DOM操作差异
  2. 实现简化版Diff算法
  3. 使用Chrome Performance分析更新过程
// 性能测试案例
const vm = new Vue({
  data: { items: ['A','B','C'] },
  template: `
    
  • {{ item }}
`
}) // 测试数据反转时的更新效率 vm.items.reverse() // 观察DOM操作次数

你可能感兴趣的:(vue.js,javascript,前端,算法)