例子
参考ArrayToArray.js中的 5.2、5.2.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,在最长递增子序列中不存在,表示要移动
实现
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--;
}
}
}
}