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] = 1,arr[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 是否有值,做出不同的处理。
- 情况1 :
newIndex === 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:迄今为止遇到的最大新索引值。
- 作用:用来判断旧列表中节点,在新列表中的 相对顺序 是否发生了变化。
- 为什么需要它:算法需要知道
move是true还是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-- |