前言

对于源码的阅读应该是建立在实际需要的过程中,这样才是科学的学习方式,不然一味地全盘阅读,先不说能不能读得完,这过程也是非常痛苦和枯燥。最近在应用虚拟列表的过程中发现vue3的更新方式给我造成了困惑,遂重温下vue2的diff算法,顺便学习下vue3的diff算法

vue2的diff算法

说一下主要流程

① 判断是否是同一个node节点

  1. 对比key
  2. 对比tag
  3. 对比comment
  4. 对比是否同一个Input、
  5. 对比placeholder

② patch

遵循一个基本原则:

先优先处理好处理的边界情况,最后全量递归对比

处理边界情况

  1. 新节点存在文本节点的情况下,当旧节点的子节点和新节点的子节点都存在的情况下,开始diff算法(也就是③),等会讲。
  2. 当旧节点没有子节点的情况下,检查新节点子节点的key的重复性.如果旧节点存在文本节点,则清空文本节点。然后批量将新节点的子节点添加到elem后面
  3. 如果旧节点存在子节点,则移除所以的子节点。
  4. 如果新节点没有子节点但是旧节点有子节点,则清空旧节点的文本内容
  5. 若新旧节点文本内容不同则替换。

③ 具体diff算法(全量递归对比)

// 递归全量对比
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(
          oldStartVnode,
          newStartVnode,
          insertedVnodeQueue,
          newCh,
          newStartIdx
        )
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(
          oldEndVnode,
          newEndVnode,
          insertedVnodeQueue,
          newCh,
          newEndIdx
        )
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(
          oldStartVnode,
          newEndVnode,
          insertedVnodeQueue,
          newCh,
          newEndIdx
        )
        canMove &&
          nodeOps.insertBefore(
            parentElm,
            oldStartVnode.elm,
            nodeOps.nextSibling(oldEndVnode.elm)
          )
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(
          oldEndVnode,
          newStartVnode,
          insertedVnodeQueue,
          newCh,
          newStartIdx
        )
        canMove &&
          nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx))
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) {
          // New element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(
              vnodeToMove,
              newStartVnode,
              insertedVnodeQueue,
              newCh,
              newStartIdx
            )
            oldCh[idxInOld] = undefined
            canMove &&
              nodeOps.insertBefore(
                parentElm,
                vnodeToMove.elm,
                oldStartVnode.elm
              )
          } else {
            // same key but different element. treat as new element
            createElm(
              newStartVnode,
              insertedVnodeQueue,
              parentElm,
              oldStartVnode.elm,
              false,
              newCh,
              newStartIdx
            )
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 新节点长度大于旧节点,批量增加新节点剩余部分到旧节点后面
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(
        parentElm,
        refElm,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      )
    } 
    //新节点长度小于旧节点,删除旧节点多余的部分
    else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }

下面用图示表示下vue2 diff的过程

深入了解Vue2和Vue3的Diff算法差异!_商城源码

下面图示表示下vue3 diff的过程

深入了解Vue2和Vue3的Diff算法差异!_递归_02

这里说下最定长稳定序列,因为大部分情况下,页面都是静态或者没有变化的元素,因此,这部分操作无疑是无效对比,浪费性能。新旧vVode列表相同的部分就是最长稳定序列。对比vue2的全量对比,提高了性能。

最长稳定序列(递增)用算法举例如下:

数列 1, 2, 3, 4, 5, 3, 2, 1, 4, 9的最长稳定(递增)序列如下;

1, 2,3,4,5

附一道最长递增(连续)序列算法

const list1 = [1, 2, 3, 4, 5, 3, 2, 8, 9, 10, 11, 12, 13, 14, 15, 3, 2, 1, 23];
const list2 = [1, 2, 3, 4, 5, 3, 2, 8];

function findMaxIncreaseList(list) {
  let start = 0;
  let end = 1;
  let tempList = [list[start]];
  let rltList = [];
  while (end < list.length) {
    if (list[end] > list[start]) {
      tempList.push(list[end]);
    } else {
      start = end - 1;
      tempList = [list[end]];
    }
    if (rltList.length < tempList.length) {
      rltList = tempList.concat();
    }
    end++;
    start++;
  }
  return rltList;
}
console.log(findMaxIncreaseList(list1));
console.log(findMaxIncreaseList(list2));

附一道最长递增(不用连续)序列算法

const list1 = [1, 2, 3, 4, 5, 3, 2, 8, 9, 10, 11, 12, 13, 14, 15, 3, 2, 1, 23];
const list2 = [1, 2, 3, 4, 5, 3, 2, 8];
const list3 = [8, 9, 10, 4, 5, 6, 7, 8, 1, 2, 3, 4, 9];

function findMaxIncreaseList(list) {
  let start = 0;
  let end = 1;
  let tempList = [list[start]];
  let rltList = [];
  while (end < list.length) {
    if (rltList.length !== 0 && list[end] > rltList[rltList.length - 1]) {
      rltList.push(list[end]);
    }
    if (list[end] > list[start]) {
      tempList.push(list[end]);
    } else {
      start = end - 1;
      if (rltList.length < tempList.length) {
        rltList = tempList.concat();
      }
      tempList = [list[end]];
    }

    end++;
    start++;
  }
  return rltList;
}
console.log(findMaxIncreaseList(list1));
console.log(findMaxIncreaseList(list2));
console.log(findMaxIncreaseList(list3));

学习附件:点此下载