28-更新element-children-4

80 阅读3分钟

例子

参考ArrayToArray.js中的 5.2、5.2.2例子

思考

其实我们已经做了首部、尾部对比,并且做了中间对比替换了,但是目前的代码存在性能上的问题

  1. 中间乱序部分会全部进行重排
  2. 乱序重排频繁使用insertBefore,性能不好

所以仍需对中间乱序排序部分做优化,在 Vue3 中,使用了最长递增子序列来获取到了稳定的序列(也就是不会变的)序列,举个例子

  • 老节点:B C D
  • 新节点:D B C
  • 其中 B 和 C 保持着一种稳定序列的关系,即 B 永远是在 C 的前面
  • 最长递增子序列的算法就是去找到某个序列中最长的稳定序列。
  • 这样可以最大程度上减少元素重排

乱序部分,即中间开始有差异的数据,我们看一个例子

  • 旧数据: [A, B, C, D, E, F, G]
  • 新数据:[A, B, E, C, D, F, G]

乱序的部分

  • 旧数据: [C, D, E] // 下标 2、3、4
  • 新数据:[E, C, D] // 基于旧数据下标排序的变成 4、3、2
  • 调用 getSequence获取到最长递增子序列在原数组中的索引是 1 2
  • 对比新节点第一项 E 对应的混乱索引是 0,在最长递增子序列中不存在,表示要移动

vue-diff-06.gif

实现

  function patchKeyedChildren(
    c1: any[],
    c2: any[],
    container,
    parentAnchor,
    parentComponent
  ) {
 // other code
 
 if(i > e1 && i <= e2){
     // other code
 } else if (i > e2 && i <= e1) {
     // other code
 } else {
 }
 
 3. 中间对比
  /** 数据等长,中间对比
       * 1. 提取新数据的key,旧数据遍历时,用来提取对应key的数据
       * 2. 遍历旧数据,找到与旧数据key对应的新数据,赋值给newIndex
       * 3. 遍历旧数据,若newIndex有值,则patch对应newIndex的数据,若没值,直接删除当前下标的旧数据
       */
      let s1 = i;
      let s2 = i;

      // 新节点的个数,用来判断遍历次数
      const toBePatched = e2 - s2 + 1;
      // patch 过的次数
      let patched = 0;

      // 当前元素是否需要移动判断标识
      let shouldMove = false;
      // 目前最大的索引
      let maxNewIndexSoFar = 0;

      // 提取新数据的key
      const keyToNewIndexMap = new Map();
      for (let i = s2; i <= e2; i++) {
        const nextChild = c2[i];
        keyToNewIndexMap.set(nextChild.key, i);
      }

      /** 创建一个定长数组,用来储存旧节点的混乱元素节点
       * 1. 初始化索引,0 表示未建立映射关系
       */
      const newIndexToOldIndexMap = new Array(toBePatched);
      for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;

      // 遍历老数据,判断当前元素是否在新数据中
      for (let i = s1; i <= e1; i++) {
        // 旧节点当前数据
        const prevChild = c1[i];
        // 新旧节点对比相同时,新节点的对应下标
        let newIndex;

        // 新老数据对比相同的次数,如果超过新数据长度,则说明是多余的数据,后面的直接删除即可
        if (patched >= toBePatched) {
          hostRemove(prevChild.el);
          continue;
        }

        // 匹配旧数据的key,匹配上返回对应的新数据下标
        if (prevChild && prevChild.key) {
          newIndex = keyToNewIndexMap.get(prevChild.key);
        } else {
          // 旧数据没有key,采用遍历新数据,再逐个对比
          for (let j = s2; j <= e2; j++) {
            if (isSameVNode(prevChild, c2[j])) {
              newIndex = j;
              break;
            }
          }
        }

        /** 匹配上数据,深度patch
         * 1. patch新老数据,更新内部改动
         * 2. patched++,记录patch调用次数,用于超出长度判断
         * 3. 储存映射索引,用于设置中间混乱数据
         */
        if (newIndex) {
          // 在储存索引的时候
          // 判断是否需要移动
          // 如果说当前的索引 >= 记录的最大索引
          if (newIndex >= maxNewIndexSoFar) {
            // 就把当前的索引给到最大的索引
            maxNewIndexSoFar = newIndex;
          } else {
            // 否则就不是一直递增,那么就是需要移动的
            shouldMove = true;
          }

          patch(prevChild, c2[newIndex], container, parentComponent, null);
          // 记录patch次数
          patched++;
          /** 设置对应旧节点在新节点上的位置
           * 1. 把新节点的索引和老的节点的索引建立映射关系
           * 2. newIndex - s2 是让下标从最左边开始排
           * 3. i + 1 是因为 i 有可能是0 (0 的话会被认为新节点在老的节点中不存在,0也是我们初始化状态)
           */
          newIndexToOldIndexMap[newIndex - s2] = i + 1;
        } else {
          // 没有找到相同数据,则删除当前数据
          hostRemove(prevChild.el);
        }
      }

      /** 最长递增子序列
       * 1. 元素是升序的话,那么这些元素就是不需要移动的
       * 2. 移动的时候我们去对比这个列表,如果对比上的话,就说明当前元素不需要移动
       * 3. 通过 moved 来进行优化,如果没有移动过的话 那么就不需要执行算法
       * 4. getSequence 返回的是 newIndexToOldIndexMap 的索引值
       * 5. 所以后面我们可以直接遍历索引值来处理,也就是直接使用 toBePatched 即可
       */

      const increasingNewIndexSequence = shouldMove
        ? getSequence(newIndexToOldIndexMap)
        : [];

      // 需要两个指针 i,j
      // j 指向获取出来的最长递增子序列的索引
      // i 指向我们新节点
      let j = increasingNewIndexSequence.length - 1;
      for (let i = toBePatched - 1; i >= 0; i--) {
        // 获取元素的索引,当前i加上左侧差异位
        const nextIndex = i + s2;
        // 获取到需要插入的元素
        const nextChild = c2[nextIndex];
        // 获取锚点
        const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null;
        if (newIndexToOldIndexMap[i] === 0) {
          // 说明新节点在老的里面不存在,需要创建
          // 因为前面初始化索引为0,如果存在0则说明,有未创建的数据
          patch(null, nextChild, container, parentComponent, anchor);
        } else if (shouldMove) {
          // 需要移动
          // 1. j 已经没有了 说明剩下的都需要移动了
          // 2. 最长子序列里面的值和当前的值匹配不上, 说明当前元素需要移动
          if (j < 0 || increasingNewIndexSequence[j] !== i) {
            // 移动的话使用 insert 即可
            hostInsert(nextChild.el, container, anchor);
          } else {
            // 这里就是命中了  index 和 最长递增子序列的值
            // 所以可以移动指针了
            j--;
          }
        }
      }
}