0. Vue3中vnodeTree进行diff的基本操作
对新旧vnodeTree进行同层比较:
- 对于没有key的vnode数组,采用的是直接数组比较方法;
- 对于有key的vnode数组,采用的是先掐头去尾,然后对剩余部分执行最长递增子序列的方法。
1. 没有绑定key的vnode子数组
没有绑定key的vnode子数组,进行patch的时候采用的是直接进行数组比较的方法。
可以看到,这种方法只是按照新旧vnode数组元素的位置索引进行一一配对,没有对vnode元素是否相同做进一步比对。即使是元素集相同的两个数组,如果元素位置被完全打乱,仍然会全部更新,没有复用。
// 基本思路:
// 1. 对oldChildren, newChildren,选取二者中长度较小的作为公共长度;
// 2. 从0位置开始,对公共长度部分一一对应直接patch;
// 3. 如果oldChildren长度更长,则把多余的部分直接unmount;
// 4. 如果newChildren长度更长,则直接把剩余的部分依次mount到最下方。
const patchUnkeyedChildren = (c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
c1 = c1 || EMPTY_ARR;
c2 = c2 || EMPTY_ARR;
// 1. 选取二者长度中的较小者,作为公共长度 commonLength
const oldLength = c1.length;
const newLength = c2.length;
const commonLength = Math.min(oldLength, newLength);
let i;
// 2. 对公共长度中的元素从头到尾1对1匹配,进行patch;
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i]));
patch(c1[i], nextChild, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
if (oldLength > newLength) {
// 3. 新数组长度小于旧数组,则需要unmount旧数组中剩余部分的vnode。
unmountChildren(c1, parentComponent, parentSuspense, true, false, commonLength);
}
else {
// 4. 新数组长度大于旧数组,则需要mount新数组剩余部分的vnode。
mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, commonLength);
}
};
2. 有key元素的diff算法
基本过程如下:
对于两个以VNode为元素的子数组oldChildren,newChildren:
设定头部指针为i,尾部指针分别为e1和e2。
- 从头部
i=0开始,一直向后匹配,直到二者vnode不相同(或到达尾部);(Vue3以key和Vnode.type都相同标记为二者相同) - 从二者各自尾部
e1=oldChildren.length-1和e2=newChildren.length-1开始,一直向前匹配,直到二者vnode不同(或遇到头部指针i); - 此时,存在三种情况:
- ① newChildren 在 oldChildren的基础上,前或后增加了若干元素(
e.g. oc = [1,2,3]; nc = [1,2,3,4,5]):i > e1 && i <= e2 - ② newChildren 在 oldChildren的基础上,前或后删除了若干元素(
e.g. oc = [1,2,3,4,5]; nc = [1,2,3]):i <= e1 && i > e2 - ③ newChildren、oldChildren在执行了前后比对之后,二者中间都剩余了部分元素(
e.g. oc = [1,2,3]; nc = [1,2,3,4,5]):i <= e1 && i <= e2
- ① newChildren 在 oldChildren的基础上,前或后增加了若干元素(
- 对于①、②两种情况,和没有key的数组比较相同,直接mount或unmount剩下的元素即可;
- 对于③情况,对二者剩余子数组oc、nc(i和e1、i和e2的中间部分),执行最大递增子序列算法:
- ① 为nc创建一个哈希表Map,键名为子元素child的key,键值为child在children中的索引;(为了减少复杂度,方便步骤②的查询)
- ② 设置一个数组
newIndexToOldIndexMap,初始化各元素为-1(Vue3中为0,oc中newIndex向后移动了1),记录nc各元素在oc中的位置:newIndexToOldIndexMap[i] = k代表nc中i位置元素,在oc中位置为k; - ③ 从头到尾遍历oc,对于它的子元素oldChild,找到它在新数组nc中的位置newIndex:
- 如果有key,从Map中查找key对应的index为newIndex,找不到则为undefined;
- 如果没有key,遍历nc,比较元素是否相同,相同则记录下它的index作为newIndex,找不到则为undefined。
- 如果newIndex为undefined,说明新子元素nc中没有这个旧元素,直接删除(unmount)当前的旧元素;
- 如果newIndex不是undefined:
- 记录它当前在oc中的位置
newIndexToOldIndexMap[newIndex] = i; - 因为nc中元素为升序排列,如果相对位置在oc中没变,那么它们在newIndexToOldIndexMap也应该是升序,因此newIndex应该是递增的:
- 记录当前已遍历的newIndex的最大值maxNewIndex,如果
newIndex >= maxNewIndex,则更新maxNewIndex; - 如果
newIndex < maxNewIndex,因为newIndex也不是undefined,则说明当前的元素移动了位置,记录下这个情况(moved = true,代表nc整体存在元素移动情况); - patch新、旧这两个元素;
- 记录当前已遍历的newIndex的最大值maxNewIndex,如果
- 记录它当前在oc中的位置
- ④ 寻找
newIndexToOldIndexMap的最大递增子序列maxSequence,它内部含有的元素,都只需要内部更新,不需要移动; - ⑤ 从后到前遍历nc:
- 对于nc最后一个元素,它mount到容器的下方。对于非最后一个元素,它的放置锚点anchor,是后一个vnode对应的DOM元素;
- 如果
newIndexToOldIndexMap[i] === -1,则说明oc中没有这个新元素,将它按照自己的放置锚点anchor,进行mount; - 否则,如果存在moved === true,而且i不在maxSequence中,则按照anchor,移动当前元素(当前元素已经在步骤③被patch过,现在只需要移动)。
这种情况,会最大程度复用vnode,只会对完全不同的元素进行更新。如果vnode顺序被打乱,根据最大递增子序列算法,会最大程度保留相对顺序没有改变的元素位置,只对剩余元素进行移位。
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1; // prev ending index
let e2 = l2 - 1; // next ending index
// [Mars] : 1. Compare the head and the tail first.
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = (c2[i] = optimized ?
cloneIfMounted(c2[i]) :
normalizeVNode(c2[i]));
if (isSameVNodeType(n1, n2)) { // type and key are the same.
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else {
break;
}
i++;
}
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1];
const n2 = (c2[e2] = optimized ?
cloneIfMounted(c2[e2]) :
normalizeVNode(c2[e2]));
if (isSameVNodeType(n1, n2)) { // type and key are the same.
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else {
break;
}
e1--;
e2--;
}
// [Mars] : 2. Compare the head and the tail first.
// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
// mount new added vnodes not contained in oldChilds.
patch(null, (c2[i] = optimized ?
cloneIfMounted(c2[i]) :
normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
i++;
}
}
}
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true);
i++;
}
}
// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i; // prev starting index
const s2 = i; // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap = new Map(); // use hashMap, cause it's easy to search an index of a key.
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized ?
cloneIfMounted(c2[i]) :
normalizeVNode(c2[i]));
if (nextChild.key != null) {
if (keyToNewIndexMap.has(nextChild.key)) {
warn$1(`Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.`);
}
keyToNewIndexMap.set(nextChild.key, i);
}
}
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
let j;
let patched = 0;
const toBePatched = e2 - s2 + 1;
let moved = false;
// used to track whether any node has moved
let maxNewIndexSoFar = 0;
// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
const newIndexToOldIndexMap = new Array(toBePatched);
for (i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0;
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true);
continue;
}
let newIndex;
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key);
} else {
// key-less node, try to locate a key-less node of the same type
for (j = s2; j <= e2; j++) {
if (newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j])) { // type and key are the same.
newIndex = j;
break;
}
}
}
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true);
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1; // n:[h,c,d,e] o:[c,d,e,h] -> newIndexToOldIndexMap: [4,1,2,3]
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
patched++;
}
}
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved ?
getSequence(newIndexToOldIndexMap) :
EMPTY_ARR;
j = increasingNewIndexSequence.length - 1;
// looping backwards so that we can use last patched node as anchor !!!!!!★★★
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) {
// mount new
patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2 /* REORDER */ );
} else {
// position is matched, no movement.
j--;
}
}
}
}
};
3. 最长递增子序列算法 getSequence()
采用的最长递增子序列算法:
// By Mars 2021.09.06
// Get max asscending sequence of an pure number array.
// eg. [2,3,6,1,7] -> [2,3,6,7]
// Algorithum (greedy) :
// Time: O(n*logn)
// Steps:
// 1. Maintain an accending order array:[result], and an array:[p] whose length is the same with given array.
// 2. result[i] = n, means that at current status, we have found a max accending sequence of length i+1, and the minimum number at the tail of the sequence is [n].
// 3. p[i] is setted when we take an element form the original given array and refresh the result array with it. p[i] records the previous number of result array of where we put the current element array[i] into at the result array;
// 4. result array is an accending array, we iterate the original array and pick current element (arr[i]), then find the first element which is larger than current element in result array (assume that position is [pos]), and replace it with current element value;
// 5. then we get the previous element of result array (result[pos-1]), and set p[i] with it;
// 6. after we iterate all the elements, the last element of result array must be the real number of the max sequence, other elements can be replaced during the precess above, so they may be not the real number of the result max sequence;
// 7. Luckily, we record the real element when we refresh every element of result, which is in array p.
// 8. everytime, we get the element of result array, and find the position of it in the original array [pos], the real previous element of max subsquence is p[pos];
// 9. repeat the step.8 until all the sequence is found.
// 目标:找到给定数组arr中最长的递增子序列
// 基本思路是:采用贪心算法,在遍历原数组的过程中,尽可能让结果子序列中的元素值增长得慢一些。
// 具体实现:
// 1。维护一个升序数组result,和一个长度与原数组arr相同的数组p;
// 2。result为在遍历过程中记录递增子序列的结果数组:如果result[i] = n,表示在当前状态下,我们至少找到了一个长度为i+1的递增序列,且序列尾部的最小值为n;
// 3。每次我们从原始数组arr中获取一个元素,并使用它更新result数组时,用p[i]记录当前元素arr[i]放入result数组的时刻,放置位置pos的前一个位置result[pos-1]的值(因为result中元素随时会被替换,p记录了当前位置元素arr[i]插入时,递增序列前一个元素的真实值,可以在最后用于真实值的回溯查找);
// 4。可知result数组一定是一个升序数组,遍历原始数组并选取当前元素arr[i],然后用二分查找在result数组中找到第一个大于当前元素的元素(假设为result[pos],没有的话把当前元素push到末尾),并将其替换为当前元素的值;
// 5。然后我们得到结果数组的前一个元素result[pos-1],并记录p[i]=result[pos-1];
// 6。遍历所有元素后,result数组的最后一个元素一定是最长递增序列的真实值,因为它是最大值,肯定不会被替换过。而其他元素都可能在上面的过程中被替换,所以它们可能不是最终结果序列的实际值;
// 7。这就是为什么当更新result中的元素时,需要在数组p中记录真实的元素值;
// 8。此时从末尾开始,每次获取result数组的元素,并查找它在原始数组arr中的位置pos,最大递增子序列的前一个真实元素值一定是p[pos];
// 9。重复这个步骤8,直到找到所有的真实最长递增子序列。
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr) {
const p = arr.slice();
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) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
}
else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}