Vue2 Diff算法
源码位置:src/core/vdom/patch.ts
源码所在函数:updateChildren()
源码讲解:
-
有新旧两个节点数组:
oldCh
和newCh
; -
有下面几个变量:
oldStartIdx 初始值=0
oldStartVnode 初始值=oldCh[0]
oldEndIdx 初始值=oldCh.length - 1
oldEndVnode 初始值=oldCh[oldEndIdx]
newStartIdx 初始值=0newStartVnode 初始值=newCh[0]
newEndIdx 初始值=newCh.length - 1
newEndVnode. 初始值=newCh[newEndIdx]
-
对比流程
- 新旧数组,从首到尾对比,直到Vnode不相同
2. 新旧数组,从尾到首对比,直到Vnode不相同
3. 旧数组尾和新数组首对比,直到Vnode不同
4. 旧数组首和新数组尾对比,直到Vnode不同
前面4步对比完成后,会有下面三种情况:
(1)旧数组没有剩余元素
针对这种情况,直接将新数组中新增的元素插入到元素6后面
(2)新数组没有剩余元素
针对这种情况,直接将旧数组中剩余的元素删除
(3)新旧数组都有剩余元素
针对这种情况,外层遍历新数组剩余Vnode,内层遍历旧数组剩余Vnode,通过双层遍历找新Vnode对应的旧Vnode:
- 没有找到对应的旧节点,则直接创建新的DOM
- 找到对应的旧节点,直接复用旧的DOM,将变化的属性更改为新的值即可
Vue3 Diff算法
patchKeyedChildren
如果新老子元素都是数组的时候,需要先做首尾的预判,如果新的子元素和老的子元素在预判完毕后,未处理的元素依然是数组,那么就需要对两个数组计算diff,最终找到最短的操作路径,能够让老的子元素尽可能少的操作,更新成为新的子元素。
旧数组
js
let c1 = [
{
id: 'a_key',
name: 'a'
},
{
id: 'b_key',
name: 'b'
},
{
id: 'c_key',
name: 'c'
},
{
id: 'd_key',
name: 'd'
},
{
id: 'e_key',
name: 'e'
}
]
let c2 = [
{
id: 'c_key',
name: 'c'
},
{
id: 'b_key',
name: 'b'
},
{
id: 'e_key',
name: 'e'
},
{
id: 'd_key',
name: 'd'
},
{
id: 'a_key',
name: 'a'
},
]
建立新节点key与其下标的映射, 保存在keyToNewIndexMap中
- keyToNewIndexMap计算源码如下:
js
// e2是c2的长度
const s1 = i;
const s2 = i;
const keyToNewIndexMap = /* @__PURE__ */ new Map();
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);
}
}
s2 = i = 0
第一遍循环:i=0,
- 代码执行完后,keyToNewIndexMap的值如下:
js
new Map([
[
"c_key",
0
],
[
"b_key",
1
],
[
"e_key",
2
],
[
"d_key",
3
],
[
"a_key",
4
]
])
keyToNewIndexMap每一项是一个对象,对象的key是新数组当前项的key(即id),对象的value是新数组当前项的index。
newIndexToOldIndexMap 记录新坐标到旧坐标的映射, 旧坐标是从1开始的。
js
let j;
let patched = 0; // 已经对比的数量
const toBePatched = e2 - s2 + 1; //需要对比的数量
let moved = false;
let maxNewIndexSoFar = 0;
const newIndexToOldIndexMap = new Array(toBePatched);
// 初始化newIndexToOldIndexMap
for (i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0;
for (i = s1; i <= e1; i++) { // 遍历旧节点数组
const prevChild = c1[i];
// 新数组已经对比完了,将旧数组中对于的节点删除
if (patched >= toBePatched) {
unmount(prevChild, parentComponent, parentSuspense, true);
continue;
}
// 找到同元素在新数组中的坐标
let newIndex;
if (prevChild.key != null) { // 元素存在key
newIndex = keyToNewIndexMap.get(prevChild.key); // 通过key获取元素在新数组的坐标
} else {
// 没有key时,遍历新数组找到新坐标
for (j = s2; j <= e2; j++) {
if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j])) {
newIndex = j;
break;
}
}
}
if (newIndex === void 0) { // 新数组中没有找到当前遍历的旧元素,则删除这个旧元素
unmount(prevChild, parentComponent, parentSuspense, true);
} else {
// 建立新坐标到旧坐标到映射
newIndexToOldIndexMap[newIndex - s2] = i + 1;
// 判断元素需要移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
patch(
prevChild,
c2[newIndex],
container,
null,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
patched++;
}
}
即新数组第1个元素在旧数组的坐标为3
js
[
3,
2,
5,
4,
1
]
在v-for循环中为什么需要key,且不能为index?
通过key可以快速的匹配相同节点。没有key的时候需要遍历新节点数组查找,导致匹配相同节点耗时久。如果key是index,则会错误的匹配相同节点,导致DOM操作增加。
increasingNewIndexSequence最长递增子序列
- 计算最长递增子序列源码
js
const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR;
js
function getSequence(arr) {
const p2 = 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) { // 对比递增子序列的最后一个元素和当前元素, 递增子序列最后一个元素小于当前元素,则将当前元素的坐标push到递增子序列中
p2[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) {
p2[i] = result[u - 1];
}
result[u] = i;
}
}
}
// 回溯修正
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p2[v];
}
return result;
}
-
最长递增子序列用到的算法
动态规划、贪心算法、二分查找、反向链表、回溯修正
-
计算后的结果
js
increasingNewIndexSequence = [1,3]
如果有移动,则执行下面代码
- 源码
js
j = increasingNewIndexSequence.length - 1;
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) { //新增节点
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2); // 执行移动操作,将nextChild移动到anchor的前面
} else {
j--;
}
}
}
新坐标到旧坐标的映射[3,2,5,4,1], 新坐标1和3保持不动,
旧坐标0(1-0)的节点移动到最末的位置,即将key为a_key的元素移动到最末的位置
旧坐标4(5-1)的节点移动到新坐标2的位置,即将key为e_key的元素移动到d_key的前面
旧坐标2(3-1)的节点移动到新坐标0的位置,即将key为c_key的元素移动到b_key的前面