Vue3 Diff 算法

1、前置知识:最长递增子序列

1.1 什么是最长递增子序列

在一个给定数值的序列(数组)中,找到一个子序列。具有如下特征:

  • 子序列的数值是依次增大的;
  • 子序列的长度尽可能的长。

比如,有一组序列 1, 3, 2, 4, 6, 5。可以找到如下的递增子序列

  • 1, 3, 4, 6

  • 1, 3, 4, 5

  • 1, 2, 4, 6

  • 1, 2, 4, 5

  • ....

1.2 贪心算法

该函数的实现核心是:贪心算法 + 二分法查找

1.2.1 贪心算法

贪心算法:每一步只做当前最优选择,不回头,不全局遍历,局部最优最终推出全局最优

针对最长递增子序列的贪心策略:

  • 同样长度的递增子序列,只保留 末尾数字最小 的那一条
  • 末尾数字越小,后面越容易接上更大的数字,更容易拼接出更长序列。

1.2.2 理解贪心优势

数组: [2,3,1]

可能的长度为 2 的递增子序列有两条:

  • 1、[2, 3]末尾: 3
  • 2、[2,1] 不递增,无效

1.2.3 贪心两条处理逻辑

arrI = 当前遍历数字,result 存各长度最优末尾下标。

1、arrresult最后一位 < arrI

当前数字比现有最长序列末尾更大,可以直接拼接在后面,序列长度 + 1。

操作:result.push(i),记录前驱 p[i] = result 末尾下标

2、arrresult 最后一位 >= arrI

不能加长序列,但当前数字更小,有优化价值。

用二分法查找到 reuslt里的第一个 >= arrI 的位置,替换该位置的下标

含义:同长度的序列,缓存更小的末尾,方便后续构造更长序列。

1.2.3 为什么贪心不能直接得到完整的 LIS(必须回溯)

还是数组 [2, 3, 1]

遍历结束后 result =[ 2, 1]

对应的值:arr[2]=1, arr[1]=3,数值递增,但是下标 2 > 1

子序列要求下标 严格 从小到大, 这条 [2,1]不符合要求。

贪心只解决两件事:

  • 1、算出最长子序列长度
  • 2、维护每种长度最小末尾,加速后续匹配

贪心不记录元素之间的先后链路,连读存在 p前驱数组里,所以必须 回溯

1.3 回溯

result贪心记录表,只是找到了最长链条的终点result最后一项),单整条链藏在 p前驱数组中。

回溯:从终点出发 ,不断找 p[last],一步一步往前倒推整条序列,最后重新写入 result 到。

示例: arr = [2, 3, 1]

遍历结束后:

  • result 记录表: [2,1], 不递增,无效

  • p前驱链表: [2, 0, 1]

最长链条的重点:last = result[result.length - 1] = 1arr[last] = 3

  • arr 中3的前驱是 arr 下标的 0 (即,p[last]) 的数 即 2

实现代码:

js 复制代码
last = result[result.length - 1]; // 最长链条的终点
const len = result.length - 1;
for(let i = len; i >= 0; i--) {
  result[i] = last; // 当前位置存入链条节点
  last = p[last];  // 跳到当前节点的前驱(上一个点)
}; 

1.4 最长递增子序列的代码实现

js 复制代码
function getSequence(arr) {
  /**
       * p数组:浅拷贝原数组。前驱索引数组
       * 
       * 执行结果: [1,0,2,1,3,5]
       * 
       * `p[i]` 的含义:以原数组第 i 个元素 `arr[i]` 作为结尾的递增子序列,它前一个元素的下标是 `p[i]`。
       * 
       * 示例1:
       * i=1 时, p[i] =  0; arr[p[i]] = 1;
       * arr[p[i]] 就是取出前驱下标对应的原始值1;
       * 语义:`[1,3]` 这条序列里,3 的前一个数字是 1
       * 
       * 
       * 示例2:
       * i=2 时,p[i]=2; arr[p[i]]=2 (等于自身);
       * 代表:没有找到可以拼接在它前面的元素 =>   以数字 2(下标 2)结尾,在当前贪心逻辑下,找不到任何一个比 2 小、能放在它前面组成更长序列的前驱;
它只能单独构成长度为 1 的子序列 [2],没有前置节点。
       * 
       */
  const p = arr.slice();
  /**
   * result:贪心容器
   * 存元素的下标,维护一个**最小末尾** 递增序列
   * 执行结果: [0,1,3,4]
   *
   * 末尾最小:末尾最小元素的下标
   * 同样长度的递增子序列,可能有很多条。我们只保留结尾数字最小的那条
   *
   */

  debugger;
  const result = [0];
  let i, j, u, v, c;

  const len = arr.length;

  for (i = 0; i < len; i++) {
    const arrI = arr[i];
    if (arrI !== 0) {
      j = result[result.length - 1];
      if (arr[j] < arrI) {
        /**
         * 满足:arr[j] < arrI 时
         * 字面:当前最长序列末尾数字 < 现在遍历到的数字
         * 业务含义:当前数字可以直接在现有最长递增序列的最后面,生成一个更长的递增子序列
         * */
        p[i] = j; // 记录:一当前 arr[i] 结尾的最长序列,前面的一个一个元素是 arr[j]
        result.push(i); // 序列长度 + 1,把当前下标存入 result,成为新的最长序列末尾
        continue;
      }

      /**
       * 不满足 arr[j] < arrI 时,
       * 字面:当前最长序列的末尾数字 >= 现在遍历到的数字
       * 业务含义:当前数字不能直接追加到现有的最长序列尾部,无法让序列变长
       *
       * 使用二分查找法优化原有序列
       * */

      // 初始下标
      u = 0;
      // 最终下标
      v = result.length - 1;

      // 只有初始下标 < 最终下标时才需要计算
      while (u < v) {
        c = (u + v) >> 1; // 即 (初始下标 + 最终下标) / 2 向下取整

        /**
         * 如果: result 中间位的值 < arrI,则 u(初始下标) = 中间位 + 1。
         * 即,从中间向右移动1位,作为初始下标(下次直接从中间喀什,往后计算)
         * */
        if (arr[result[c]] < arrI) {
          u = c + 1;
        } else {
          /**
           * 否则,则 v(最终下标)= 中间位。
           * 即:下次直接从 0 开始,计算到中位置。
           * */
          v = c;
        }
      }

      // 最终,经过 while 的二分运算可以计算出:目标下标位 u

      // 如果 arrI < arr[result[u]] ,则证明当前 result 中存在的下标 **不是** 递增序列,需要进行替换
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1];
        }

        result[u] = i;
      }
    }
  }

  /**
   * 回溯,还原完整最长递增子序列
   * result 在二分贪心走完后,里面的下标并不是一条连续、真实存在的递增子序列,不能直接拿来用。
   * 必须依靠 p 前驱数字反向追溯,才能把 result 重写成一条完整、真实的最长递增子序列下标链(这个过程就是回溯)。
   *
   *
   * 示例:data = [2,3,1]
   * 遍历+二分查找结束后,回溯之前
   * result = [2,1]
   * p = [0,0,0]
   *
   * 对应数值 data [2]=data [1]=3 → [1,3],数值确实递增,
   * 但 **下标 2 在原数组里出现在下标 1 后面**,原数组顺序是 2,3,1,
   * [2,1] 这组下标根本不是原数组里一条合法的子序列!
   * 子序列要求下标严格从小到大,2>1,非法。
   *
   * 这就是必须回溯的核心原因:
   * 贪心二分跑完的 result 只是 【各长度最小末尾记录表】,内部下标顺序 **不保证时原先数组先后顺序**,不能直接当 LIS 使用。
   *
   * 执行回溯,修复 result
   * 分步跑:
   * 初始 last = 1
   * i=1:result[1] = 1;last = p[1] = 0
   * i=0:result[0] = 0;last = p[0] = 0
   * 循环结束,result 被覆盖成:[0, 1]
   *
   */
  //  重新定义 u。此时 u = result.length
  u = result.length;
  // 重新定义 v。v = reult 的最后一个元素
  v = result[u - 1];
  // 自后向前处理 result,利用 p 中所保存的索引值,进行最后一次回溯
  while (u-- > 0) {
    result[u] = v;
    v = p[v];
  }

  console.log('result', result);
  console.log('p', p);
  return result;
}

debugger;
var arr1 = [1, 3, 2, 4, 6, 5];
console.log(getSequence(arr1)); // [0,2,3,5]
var arr2 = [2, 3, 1];
console.log(getSequence(arr2)); // [0,1]

2、Vue Diff 的整体流程

执行 patchKeyedChildren 方法

js 复制代码
function patchKeyedChildren(oldChildren, newChildren, container, parentAnchor) {
  let i = 0;
  let oldChildrenEnd = oldChildren.length - 1;
  let newChildrenEnd = newChildren.length - 1;

  // 新子节点列表的长度
  const newChildrenLength = newChildren.length;

  // 1、前序比较:自前向后
  while( i <= oldChildrenEnd && i<= newChildrenEnd) {
    
  }

  // 2、后序比较:自后向前
  while(i <= oldChildrenEnd && i<= newChildrenEnd) {
    
  }

  // 3、新节点 比 旧节点多,挂载新节点'
  /**
  * i 表示:剩余区间的左边界
  * oldChidren 表示:旧节点剩余区间的右边界
  * [i, oldChildren] 表示:旧节点的剩余待处理区间
  *
  *  当 i > oldChildren 为 true 时
  *  表示 旧节点区间为空,即旧节点全部处理完成了。后续只要处理新节点即可。
  *
  */
  if(i > oldChildrenEnd) {
    
  }
  // 4、旧节点 比 新节点多,卸载旧节点
  /**
  * i 表示:剩余区间的左边界
  * newChildrenEnd 表示:新节点剩余区间的右边界
  * [i, newChildrenEnd]:表示新节点的剩余待处理区间
  *
  *  i > newChildrenEnd 为 true 时
  *  表示 新节点区间为空,即新节点全部全部处理完成。后续只要卸载旧节点即可。
  */
  else if(i > newChildrenEnd) {
    
  }
  else {
    // 5、乱序比较,核心 Diff 算法
  }
}

2.1 前序比较(同步头部): 自前向后比较

js 复制代码
while(i <= oldChildrenEnd && i <= newChildrenEnd) {
  const oldVNode = oldChildren[i];
  const newVNode = normalizeVNode(newChildren[i]);
  if(isSameVNode(oldVNod, newVNode)) {
    patch(oldVNode, newVNode, container, parentAnchor);
    i++; // ✅
  }else {
    break;
  }
}

2.2 后序比较(同步尾部):自后向前比较

js 复制代码
while(i <= oldChildrenEnd && i <= newChildrenEnd) {
  const oldVNode = oldChildren[oldChildrenEnd];
  const newVNode = normalizeVNode(newChildren[newChildrenEnd]);
  if(isSameVNode(oldVNode, newVNode)) {
    patch(oldVNode, newVNode, container, parentcAnchor);
    oldChildrenEnd--; // ✅
    newChildrenEnd--; // ✅
  }else {
    break
  }
}

2.3 旧节点列表跑空:新节点 比 旧节点多,挂载

js 复制代码
if(i > oldChildrenEnd) {
  if (i <= newChildrenEnd) {
    const nextProps = newChildrenEnd + 1;
    const anchor = nextProps < newChildrenLength ? newChildren[nextProps].el : parentAnchor;
    while(i <= newChildrenEnd) {
      const newVNode = normalizedVNode(newChildren[i]);
      patch(null, newVNode, container, anchor);
      i++;// ✅
    }
  } 
}

2.4 新节点列表跑空:旧节点 比 新节点多,卸载

js 复制代码
else if( i > newChildrenEnd) {
  while(i <= oldChildrenEnd) {
    const oldVNode = oldChildren[i];
    // 卸载多余的旧节点
    unmount(oldVNode);
    i++; // ✅
  }
}

2.5 乱序 Diff算法

贪心算法 + 二分法查找

js 复制代码
else {
  
  const oldStartIndex = i; // 旧子节点开始索引
  const newStartIndex = i; // 新子节点开始索引
}

2.5.1 建立 "新节点的key 到索引"的映射表

js 复制代码
else {
  // 省略代码...
  // 5.1 创建一个 <key(新节点的key): index (新节点的位置)>的 Map 对象 keyToNewIdexMap
  const keyToNewIndexMap = new Map();
  for(i = newStartINdex; i<= newChildrenEnd; i++) {
    const nextChild = normalizeVNode(newChildren[i]);
    if(nextChild.key != null) {
      keyToNewIndex.set(nextChild.key, i)
    }
  }
}

建立 keyToNewIndexMap映射表的作用:

1、核心作用:性能提升,实现 O(1) 的精准查找

在后续的 Diff 阶段,需要遍历 旧子节点列表 for(i = oldStartIndex; i <= oldChildrenEnd; i++){ }

对于遍历到的每一个旧子节点,都要检查:

  • 这个旧节点,在新列表中是否存在?

  • 如果存在,它的在新子节点列表中的位置索引是多少?

如果没有有映射表: 只能调用 newChildren.findIndex() 或 双层 for 循环。这会导致 O(n²) 的时间复杂度。当列表过多时,性能会急剧下降。

有了映射表: 直接通过 keyToNewIndex.get(prevChild.key) 取值。无论列表多长,都只需要一次哈希查找,瞬间完成。

2、区分 "复用/更新"与"卸载/删除"

拿到 查找结果(newIndex,旧节点在新列表中的索引)后,判断 newIndex 是否有值,做出不同的处理。

  • 情况1newIndex === undefined,即在映射表中没有查找到。

这意味着这个旧节点在新子节点列表中彻底消失 了,应立即执行 unmount(preChild) 卸载这个旧节点。

  • 情况2:newIndex 有值,即在映射表中查找到了。

这意味着旧这个旧子节点在新子节点列表中依然存在, 进入 patch(prevChild, newChildren[index]) 打补丁的逻辑。

3、为"移动判断"提供数据源

当找到 newIndex 后,会执行这行代码:

js 复制代码
newIndexToOldIndexMap[newIndex - newStartIndex] = i - 1;

newIndexToOldIndexMap数组,记录了 "新列表中的每个节点,对应旧列表中的位置"

这个数组正是后续计算最长递增子序列(LIS)的唯一数据源 。只有依赖映射表查找出的 newIndex ,才能把旧索引(i)填入新数组的正确位置,最终推断出哪些节点是不要移动的。

2.5.2 遍历旧节点,打补丁并记录位置映射

js 复制代码
else {
  // 省略代码...
  // 5.2 循环 oldChilren,并尝试进行 patch(打补丁)或 umount(卸载旧节点)
    let j = 0; // 后续遍历新节点列表使用
    // 记录已经修复的新节点的数量
    let patched = 0;
    // 新节点需要修补的数量
    const toBePatched = newChildrenEnd - newStartIndex + 1;
    // 标记位:节点是否需要移动
    let moved = false;
    // 迄今位置遇到的最大新索引值:用来判断旧节点,在新列表中的相对顺序是否发生了变化
    let maxNewIndexSoFar = 0;
    /**
     * 新旧节点索引地图:追踪 旧节点的去向。
     * 作用1:识别 "纯新增节点",进行挂载
     * 作用2:计算 "最长递增子序列" LIS,最小化移动
     */
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
    // 遍历 oldChildren (旧节点列表)
    for (i = oldStartIndex; i <= oldChildrenEnd; i++) {
      // 获取旧节点
      const prevChild = oldChildren[i];
      // 如果当前已处理的节点数量 >= 待处理节点的处理,则 **所新节点都已经处理完成,剩余的旧节点全部卸载即可**
      if (patched >= toBePatched) {
        unmount(prevChild);
        continue;
      }
      // 旧节点在新节点列表中的索引
      let newIndex;
      // 旧节点的 key 存在时
      if (prevChild.key != null) {
        // 从 映射表(keyToNewIndexMap)中获取到 旧节点在新列表中的索引
        newIndex = keyToNewIndexMap.get(prevChild.key);
      } else {
        // 旧节点的 key 不存在时
        // 遍历新节点列表
        for (j = newStartIndex; j <= newChildrenEnd; j++) {
          if (
            newIndexToOldIndexMap[j - newStartIndex] === 0 && // ❓
            isSameVNode(prevChild, newChildren[j])
          ) {
            // 旧节点 找到了 在新列表中的索引
            newIndex = j;
            break;
          }
        }
      }

      // 判断旧节点是否找到了索引
      if (newIndex === undefined) {
        // 没有找到,直接卸载
        unmount(prevChild);
      } else {
        /**
         * 当前旧节点找到了对应的新节点,
         * 接下来就是判断 对应的新节点 时 patch 还是 move
         */
        /**
         * 给 newIndexToOldIndexMap 赋值
         * 1、键:新节点的相对索引 ==> 值:旧节点中的索引
         * 2、newIndexToOldIndexMap 的下标值不为 0,新节点已匹配到旧节点了。
         * 
         * 因为 newIndex 包含了 已经处理的节点,所以需要减去 newStartIndex :表示不计算已处理的节点
         * */ 
        newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1;
        if (newIndex >= maxNewIndexSoFar) {
          // 持续递增
          maxNewIndexSoFar = newIndex;
        } else {
          // 没有递增,表示需要移动
          moved = true;
        }

        // 打补丁
        patch(prevChild, newChildren[newIndex], container, null);
        patched++;
      }
  
}
1、maxNewIndexSoFar 的核心作用

maxNewIndexSoFar迄今为止遇到的最大新索引值

  • 作用:用来判断旧列表中节点,在新列表中的 相对顺序 是否发生了变化。
  • 为什么需要它:算法需要知道 movetrue 还是 false。如果是 false ,则说明新旧节点顺序完全一致,可以直接跳过复杂的 "最长递增子序列"计算,节省性能。
判断逻辑:

在遍历旧节点时,会执行下面的代码

js 复制代码
if(newIndex >= maxNewIndexSoFar) {
    maxNewIndexSoFar = newIndex;
}else {
  moved = true; ; // 发现异常:当前节点的位置比之前遇到的某个节点更靠前!
}
判断原理:

我们按照 旧列表的顺序 (从前往后)去新列表张查找它们的位置(newIndex)。

  • 理想情况(顺序没变) :因为旧列表的顺序就是原来 DOM 顺序,如果新列表只是修改了内容,但没打乱顺序,那么这写节点在新列表中也应该 依次递增 的。每次 newIndex 都会大于等于 maxNewIndexSoFar,所以 moved 保持 false
  • 异常情况(顺序乱了) :如果某个旧节点在新列表中的位置(newIndex)小于之前某个旧节点在新列表中的位置(maxNewIndexSoFar),说明这个旧节点在新列表中被 移动 到前面去了,顺序发生改变,需要将 moved标记位 true
2、newIndexToOldIndexMap (新旧节点索引地图)的作用
  • 键:新节点的 相对索引
  • 值:旧节点的索引
核心作用:在遍历阶段,追踪"旧节点的去向"

在遍历旧节点的循环中(for(i = oldStartIndex; i <= oldChildrenEnd; i++)),每处理一个旧节点,旧会通过个人 key映射表keyToNewIndexMap)里查找它在新列表中的位置(newIndex)。

找到之后,对 newIndexToOldMap 进行赋值操作 :

newIndexToOldIndwexMap[newIndex - newStartIndex] = i + 1

这个操作本质时:将 "旧节点的位置"填到 "新节点位置"对应的格子中

为什么值要 +1?(数据编码)

代码中有一行非常关键:newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1

  • 数组的下标:代表 "新列表中的相对位置"(从 0 开始计数)
  • 数组的值:代表 "就旧列表中绝对索引 + 1"

为什么要加 1?

因为 0 被预留作为特殊标记 。在初始化时,所有值都被设为 0。当值为 0 时,表示 "这个新节点在旧列表中完全找不到对应的旧节点,它是一个全新的节点 ,需要挂载。"。因此,如果旧节点索引是 0,不加 1 直接存 0 ,就和 "找不到"混淆了。

后续两大用途

用途一:识别"纯新增节点"(挂载)

再后续的倒序遍历中(for(i = toBePatched - 1; i>= 0; i++)),代码会检查:

js 复制代码
if(newIndexToOldIndexMap[i] === 0) {
  patch(null, nextChild, container, anchor);
}

如果地图中某个格子的值是 0,意味着没有旧节点可复用,直接创建新 DOM 插入。

用途二:计算"最长递增子序列"(最小化移动)

地图构建完成后,getSequence(newIndexToOldIndexMap) 会对这个数组(假设是:[5,3,4,2])计算最长递增子序列(LIS)。

  • 这个数组的 LIS 是 [3,4];对应地图的索引是 1,2。
  • 对应新节点列表中的 索引 1 和 索引 2 节点,不需要移动。
示例

假设剩余区间:

  • 旧列表[B, C, D, E](旧索引:B=1, C=2, D=3, E=4)
  • 新列表[E, C, D, B](新索引:E=0, C=1, D=2, B=3)

遍历旧列表,填表过程如下:

遍历旧节点 查找的 newIndex 填表位置(新相对索引) 填入的值(旧索引 + 1) 此时数组的状态
初始 - - - [0,0,0,0]
B 3 3 1 + 1 [0,0,0,2]
C 1 1 2 + 1 [0,3,0,2]
D 2 2 3 + 1 [0,3,4,2]
E 0 0 4 +1 [5,3,4,2]

此时,newIndexToOldIndexMap = [5, 3, 4, 2] 的含义非常清晰:

  • 新列表第 0 位(E):原本在旧列表第 4 位(值 5-1=4)。
  • 新列表第 1 位(C):原本在旧列表第 2 位(值 3-1=2)。
  • 新列表第 2 位(D):原本在旧列表第 3 位(值 4-1=3)。
  • 新列表第 3 位(B):原本在旧列表第 1 位(值 2-1=1)。
3、newIndexToOldIndexMap[j - newStartIndex] === 0 互斥锁

判断 newIndexToOldIndexMap[j - newStartIndex] === 0 ,是为了给 "新节点"加一把 "已占用锁" ,确保一个萝卜一个坑,防止多个旧节点重复匹配同一个新节点

  • 下标值为 0 :新节点还没匹配到旧节点
  • 下标值 > 0:新节点已经匹配到旧节点了
示例
  • 旧列表(无 Key)[div, div]
  • 新列表(无 Key)[p, div]

索引地图(newIndexToOldIndexMap)初始值 [0,0]patched = 0

步骤 操作 不检查 0 (错误) 检查 0 (正确)
第1步 第一个 旧 div找配对 找到新列表的 div (下标 1),配对成功,索引地图:[0, 1]``patched= 1 找到新列表的 div (下标 1),配对成功,索引地图:[0, 1]``patched= 1
第2步 第二 旧 div找配对 又找到新列表的 div(下标1),抢走 。索引地图:[0, 2]第二个 旧 div 是更新,而没有卸载。patched= 2 又找到新列表的 Div(下标1),发现已被占(1) ,跳过。 结果:找不到配对,旧的第二个 Div 被卸载。 新列表下标0的 P 检测到登记表为 0,执行新建patched= 1
最终结果 ❌ 页面多了一个 div

2.5.3 计算最长递增子序列(LIS)

js 复制代码
// 仅当节点需要移动的时候,生成最长递增子序列
const increasingNewIndexSequence = moved 
  ? getSequence(newIndexToOldIndexMap) 
  : EMPTY_ARR;
1、最长递增子序列的目的:实现最小化移动

找出数组中的最长递增子序列,这些节点可以被原地复用不需要移动 ,剩下的节点才需要做 DOM 移动操作,极大地减少了 DOM 的操作次数

2.5.4 从后往前遍历toBePatched,执行挂载和移动

toBePatched 表示:新节点需要修补的数量

js 复制代码
 // 5.4 从后往前遍历 `toBePatched` ,执行挂载和移动
    /**
     * j >= 0:初始值 为 最长递增子序列的最后一个下标
     * j <0:**不存在** 最长递增子序列
     *
     */
    j = increasingNewIndexSequence.length - 1;
    // 倒序遍历:新节点需要修补的数量
    for (i = toBePatched - 1; i >= 0; i--) {
      // 通过 nextIndex 拿到需要处理的新节点
      const nextIndex = newStartIndex + i;
      const nextChild = newChildren[nextIndex];
      // 获取锚点
      const anchor =
        nextIndex + 1 < newChildrenLength
          ? newChildren[nextIndex + 1].el
          : parentAnchor;

      /**
       * newIndexToOldIndexMap[i] === 0 时,表示:
       * 新节点**没有**匹配到对应的旧节点,该新节点需要挂载
       */
      if (newIndexToOldIndexMap[i] === 0) {
        // 挂载新节点
        patch(null, nextChild, container, anchor);
      }
      // moved 为 true 时,表示需要移动
      else if (moved) {
        /**
         *  j < 0 :不存在 最长递增子序列;
         *  i !== increasingNewIndexSequence[j]:当前节点不在 最长递增子序列 中,
         *
         * 此时需要移动(move)
         */

        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor);
        } else {
          // j 随着循环递减
          j--;
        }
      }
    }
1、i < increasingNewIndexSequence[j]的作用

i !== increasingNewIndexSequence[j] 这行代码的作用是:判断当前遍历到的这个新节点,是否属于"不需要移动"的免移名单(最长递增子序列)

  • 如果 不相等true):说明当前节点不在免移名单里,需要执行 DOM 移动move)。
  • 如果 相等false):说明当前节点在免移名单里,不需要移动,保持原地不动。
示例
  • toBePatched : 6;表示有 6个新节点需要处理。
  • moved: 为 true 表示需要移动
  • newIndexToOldIndexMap[1,3,2,4,6,5]

newIndexToOldIndexMap

  • 下标:新节点的相对索引位置
  • 值:旧节点的索引位置(值 - 1)

通过 getSequence 计算得出 最长递增子序列

  • increasingNewIndexSequence[0,2,3,5]
  • j = increasingNewIndexSequence.length - 1 : 初始值为 3,表示 最长递增子序列的最后一个元素的索引。

倒序遍历 toBePatched的步骤:

步骤 i :当前需要处理的 新节点的相对索引 j :当前最长递增子序列 最后一个元素的索引 newIndexToOldIndexMap[i] :新节点相对索引对应的旧节点索引 increasingNewIndexSequence[j] :新 节点的相对索引 i increasingNewIndexSequence[i] 比较的结果
第1步 5 3 5 5 true ,不需要移动。执行j--
第2步 4 2 6 3 需要移动
第3步 3 2 4 3 true ,不需要移动。执行j--
第4步 2 1 2 2 true ,不需要移动。执行j--
第5步 1 0 3 0 需要移动
第6步 0 0 1 0 true ,不需要移动。执行j--
相关推荐
胡萝卜术1 小时前
抽象的三级跳:从原生 DOM 到 React 组件树,我们到底在解决什么问题?
前端·javascript·面试
黄敬峰1 小时前
从零理解React:事件、组件与响应式——一个WebGPU Demo的前端笔记
前端·面试
xn71331 小时前
AI SDK 7 迁移实战:TypeScript 通过后,生产环境还会坏在哪里?
vue.js·人工智能·后端
weedsfly1 小时前
从类继承到自定义 Hook:模板方法模式的前端演进
前端·javascript·面试
Quz1 小时前
QML 线性布局:Row/RowLayout 与 Column/ColumnLayout
前端·qt
你为她披上外套时我正站在窗外1 小时前
用 Rust + WebAssembly 写了个字幕工具,文件全程不离开浏览器
前端
帅帅哥的兜兜1 小时前
AI模型初了解
前端
bonechips1 小时前
React + WebGPU(二):合成事件与进度条组件封装
前端·react.js
我是大卫1 小时前
【图】React源码解析-从底层数据结构、渲染调度到闭包原理深挖useRef的核心机制
前端·react.js·源码