目录
前言
本章节,我们将一起阅读源码,观看VUE3底层对vNode和列表的比较是怎么进行的,以及为什么列表需要设置key值。
旧vNode 和 新 vNode 的对比
主要是判断是否是同一vNodeType
function isSameVNodeType(n1, n2) {
...
return n1.type === n2.type && n1.key === n2.key;
}
type: div、button、span等标签,也可能是当它为组件时,是object
key:就是 :key 的值, 例如: <button :key=12> 中 key 就是12
两个列表【Children】的对比
有设置key值的对比
- 第一步,从头部(i=0)开始,比较到不同的地方,break出来
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1; // prev ending index
let e2 = l2 - 1; // next ending index
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = c2[i];
if (相同) { 挂载}
else { break;}
i++;
}
- 第二步,这时i的值为前面不同点处,再从尾部(i=length-1)开始比较到不同之处,break出来。
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1];
const n2 = c2[e2];
if (相同) {挂载}
else {break;}
e1--;
e2--;
}
- 第三步,前两步主要是把范围向内缩小,当两边已经相遇的时候,新列表 e2 又比 旧列表e1 多一些数据,即列表新增了几条数据的话,直接挂载这部分的数据
// 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) {
patch(null, (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
i++;
}
}
}
- 第四步,当两边已经相遇的时候,新列表 e2 又比 旧列表e1 少一些数据,即列表删除了几条数据的话,直接卸载这部分的数据
// 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++;
}
}
- 第五步,第一步和第二步是收敛,第三步和第四步是处理已经相遇的情况,那么第五步,就是处理没有相遇的情况
1. 计算还应挂载多少个child,然后新建一个,遍历新数据设置 keyToNewIndexMap;
2.新建newIndexToOldIndexMap变量,遍历旧数据列表,如果存在在新的列表中,就挂载,设置newIndexToOldIndexMap为新的值;如果查询不到,或者已经足够挂载数了,就直接卸载掉;
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) { //已经达到挂载数了,其他卸载掉 unmount(...); continue; } let newIndex; //todo 根据 keyToNewIndexMap(或者遍历判断isSameVNodeType),获取newIndex ... if (newIndex === undefined) { //不存在新的列表中了 unmount(...); } else { //存在挂载,设置maxNewIndexSoFar或标记move,为了后续move的时候做处理 newIndexToOldIndexMap[newIndex - s2] = i + 1; if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex;} else { moved = true;} patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized); patched++; } }
3.根据newIndexToOldIndexMap出处理新队列,判断是新增的就新增,或者对dom做move处理。
没有设置key值的对比
这块没有key值对比就比较粗暴,把之前位置一对一比较,直接挂载(patch:patch内部会做isSameVNodeType比较,不一样就卸载);长度进行比较。多了挂载,少的卸载
const patchUnkeyedChildren = (c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
c1 = c1 || EMPTY_ARR;
c2 = c2 || EMPTY_ARR;
const oldLength = c1.length;
const newLength = c2.length;
const commonLength = Math.min(oldLength, newLength);
let i;
for (i = 0; i < commonLength; i++) {
...
patch(c1[i]...);
}
if (oldLength > newLength) {
// remove old
unmountChildren(c1, parentComponent, parentSuspense, true, false, commonLength);
}
else {
// mount new
mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, commonLength);
}
};
总结
经过本章节的说明,其实可以看出列表设置键值key的重要性,而且key值应是独一的,具有个人标识符,例如:id;这样在列表的比较的时候,可以更好的复用之前的vNode。当只是移动、删除时,可以仅更改到当前vNode。