10 双端比较

10.1 双端比较的原理

双端比较通过定义新旧节点的开始下标与结束下标四个变量。然后进行对比

  1. 新节点开始下标的节点和旧节点开始下标的节点对比
  2. 新节点结束下标的节点和旧节点结束下标的节点对比
  3. 新节点结束下标的节点和旧节点开始下标的节点对比
  4. 新节点开始下标的节点和旧节点结束下标的节点对比

代码如下

function patchKeyedChildren(n1,n2,container){
  const oldChildren = n1.children
  const newChildren = n2.children
  // 四个索引值
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  // 四个索引指向的vnode节点
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]
  while(oldStarIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(oldStartVNode.key === newStartVNode.key){
        patch(oldEndVNode,newEndVNode,container)
        // 不需要移动dom 移动下标和元素即可
        oldStartVNode = oldChildren[++oldStartIdx]
        newStartVNode = newChildren[++newStartIdx]
      } else if(oldEndVNode.key === newEndVNode.key) {
        patch(oldEndVNode,newEndVNode,container)
        // 也不需要移动
        oldEndVNode = oldChildren[--oldStartIdx]
        oldStartVNode = newChildren[--newStartIdx]
      } else if(oldStartVNode.key === newEndVNode.key) {
        // 进行patch
        patch(oldStartVNode,newEndVNode,container)
        // 因为旧的开始变为新的结束 因此需要将
        insert(oldStartVNode,container,oldEndVNode.el.nextSibing)
        // 移动下标 更新元素
        oldStartVNode = oldChildren[++oldStartIdx]
      } else if(oldEndVNode.key === newStartVNode.key) {
        // 先进行patch
        patch(oldEndVNode,newStartVNode,container)
        // 再进行移动DOM操作
        // 因为旧的最后一个变成新的第一个,因为需要移动到第一个元素前面
        insert(oldEndVNode.el, container, oldStartVNode.el)

        //移动下标和元素
        oldEndVNode = oldChildren[--oldEndIdx]
        newStartVNode = newChildren[++newStartIdx]
      } 
  }
}

处理新增和删除情况

  1. 处理在循环内部

    在while循环中增加一个else处理在新节点在旧节点的情况

    const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)
    if(idxInOld > 0) {
      patch(oldChildren[idxInOld],newStartVNode,container)
      // 将元素移动到最前面
      insert(oldChildren[idxInOld].el,container, oldStartVNode.el)
      // 由于idxInOld 对应元素已经移动 所以需要将原来的位置对应元素置为undefined
      oldChildren[idxInOld] = undefined
      newStartVNode = newChildren[++newStartIdx]
    } else {
      // 将newStartVNode作为新节点挂载到头部,使用当前头部节点 oldStartVNode.el作为锚点
      patch(null,newStartVNode,container)
      newStartVNode = newChildren[++newStartIdx]
    }

    因为旧节点可能出现undefined情况,因此需要跳过旧节点为假的情况,需要在循环内最前面添加判断

    if(!oldStartVNode) {
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if(!oldEndVNode) {
      oldEndVNode = oldChildren[--oldEndIdx]
    }
  2. 处理循环外部的新增和删除情况

    首先看新增情况,如果新节点开始下标小于新节点结束下标,因此在循环后面增加判断

    if(newStartIdx <= newEndIdx && oldStartIdx > oldEndIdx) {
      for(let i = newStartIdx; i <= newEndIdx;i++) {
        patch(null,newChildren[i],container, oldStartVNode.el)
      }
    }

    接着处理删除情况,如果旧节点开始下标小于旧节点技术下标,需要把多余的节点删除,因此增加判断

    else if(oldStartIdx <= oldEndIdx && newStartIdx > newEndIdx) {
       for(let i = oldStartIdx; i <= oldEndIdx, i++) {
         unmount(oldChildren[i])
       }
    }