vue-diff算法之原理——下

97 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

vue-diff算法之原理——上

vue-diff算法之原理——中

上集说到前后先等的部分已经处理完毕了,现在剩下的是一些不是特别规律的两组新老节点数组。

用新节点的key映射新节点的绝对索引位置

为了尽可能的复用,又尽可能降低算法复杂度,首先是准备新的节点的数据格式改成key:value的映射,在map里,key是新的节点的key,value是这个节点在新列表中的真实位置。 从未处理的新节点的起始位置开始到新节点未处理的节点结束位置为止。都映射到map结构中; 此时开始位置都为i,但是从现在起要单独处理,所以记录自己的指针。

   // 新老节点都还有,可能是乱序,也可能是全部替换
    const s1 = i;
    const s2 = i;
    // 新节点的映射对象
    const keyToNewIndexMap = new Map();
    for (i = s2; i <= e2; i++) {
      const nextChild = c2[i];
      keyToNewIndexMap.set(nextChild.key, i)
    }

初始化老节点相对新节点可复用的位置

新增节点的个数。结束位置-开始位置+1,当前也可以直接读取 keyToNewIndexMap 的size属性,初始化一个新增加节点长度的数组,记录每个节点复用的老节点的下标。

  // 新增节点的总个数 结束位置-开始位置+1
    const toBePatched = e2 - s2 + 1;

    // 下标是新元素的相对下标,初始值是0,如果节点复用了,值是老元素的下标
    const newIndexToOldIndexMap = new Array(toBePatched);
    for (i = 0; i < toBePatched; i++) {
      newIndexToOldIndexMap[i] = 0;
    }

更新所有新节点,并计算是否需要移动

maxNewIndexSoFar 记录一下最大索引,来判断可复用的节点是否是有序的,如果可以复用的节点都是有序的,moved就一直是false,代表没有需要移动的节点,反过来就有要移动的节点。 patched代表已经处理了的更新的个数,如果已经更新的节点个数大于或等于了需要处理的节点数,那么剩下的老节点全都可以移除了 遍历未处理的老节点。如果新节点map中没有可以复用的节点,直接卸载这个老节点的dom。如果有可以复用的老节点。当这节点的索引大于上一个处理的节点时,说明这两个节点的顺序是正确的,只要记一下这个节点的索引为最大索引就行了。否则就将moved标记为true,在newIndexToOldIndexMap中记录新节点相对位置的值是老节点绝对下标+1;也就是常说的第几位。执行patch方法更新dom属性,patched个数加1.

    let patched = 0;
    let moved = false;
    let maxNewIndexSoFar = 0;
    // 遍历老节点,找到尽可能多的可以复用的节点,并且干掉多余的节点

    for (i = s1; i <= e1; i++) {
      const prevChild = c1[i];
      if (patched >= toBePatched) {
        unmount(prevChild.key);
        continue;
      }
      const newIndex = keyToNewIndexMap.get(prevChild.key);
      if (newIndex === undefined) {
        unmount(prevChild.key);
      } else {
        // 节点复用,不需要移动
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex;
        } else {
          moved = true;
        }
        // 新节点相对下标对应的是老节点下标+1

        newIndexToOldIndexMap[newIndex - s2] = i + 1;
        patch(prevChild.key);
        patched++;
      }
    }

移动需要移动的节点

本着最少操作完成最大复用的目的,如果无需移动节点 最长子序列 increasingNewIndexSequence 为一个空数组,否则就要计算出最长子序列的索引的数组,因为这些元素是无需移动的,只移动其他的没有顺序的节点就可以最快的完成排序

image.png 当前新老节点未处理的节点就是上面红框圈出部分。肉眼可以看到key未c,d的是,最长子序列,是稳定排序的,不用移动,然后只要将e节点移动到前面就可以了

从后面开始遍历未处理的节点,获取绝对索引,拿到新节点。判断节点是否有复用的老节点,如果没有就直接创建个新的dom.否则,判断稳定的节点(无需移动节点)的索引是否大于0,并且当前处理的索引不是当前期望的稳定节点的下标。那么就移动这个节点,否则稳定节点索引减1.

    // 从这个例子里 getSequence(newIndexToOldIndexMap) 返回了[1,2]
    const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [];
    let lastIndex = increasingNewIndexSequence.length - 1;
    // 从后面开始遍历要新插入的节点
    for (i = toBePatched - 1; i >= 0; i--) {

      // 获取绝对索引
      const newChildIndex = s2 + i;
      // 拿到新的节点
      const nextChild = c2[newChildIndex];
      // 判断节点是不是mount
      if (newIndexToOldIndexMap[i] === 0) {
        // 没有复用
        mountElement(nextChild.key);
      } else {
        // move
        // 有复用
        if (lastIndex < 0 || i !== increasingNewIndexSequence[lastIndex]) {
          move(nextChild.key)
        } else {
          lastIndex--;
        }
      }
    }

vue diff算法大概就是这个逻辑。 getSequence 这个方法暂时还没实现。