本文基于Vue 3.2.30
版本源码进行分析
为了增加可读性,会对源码进行删减、调整顺序、改变部分分支条件的操作,文中所有源码均可视作为伪代码
由于ts版本代码携带参数过多,不利于展示,大部分伪代码会取编译后的js代码,而不是原来的ts代码
整体概述
从Vue 3.2.30
的源码可以知道,源码中有对patchKeyedChildren()
方法进行了核心步骤的注释,摘出核心步骤的注册如下面代码块所示,可以分为5个步骤:
步骤1
:从头->尾,处理相同的前置元素步骤2
:从尾->头,处理相同的后置元素步骤3
:旧vnode
已经处理完毕,但是新vnode
还有元素,处理新增元素,直接进行mount
步骤4
:旧vnode
还有剩余,但是新vnode
已经处理完毕,处理已经废弃元素,直接进行unmount
步骤5
:最复杂的情况,处理相同的前置元素+处理相同的后置元素后,剩下的元素有新增
、废弃
、乱序
的情况,需要复杂处理
const patchKeyedChildren = (c1, c2, container, ...args) => {
// 1. sync from start
// (a b) c
// (a b) d e
// 2. sync from end
// a (b c)
// d e (b c)
// 3. common sequence + mount
// (a b)
// (a b) c
// 4. common sequence + unmount
// (a b) c
// (a b)
// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
}
步骤1:从头->尾,处理相同的前置元素
直接从i=0
开始遍历,如果找到相同类型的元素,直接进行patch()
进行数据的更新,然后递增i
const patchKeyedChildren = (c1, c2, container, ...args) => {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1; // prev ending index
let e2 = c2.length - 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 (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else {
break;
}
i++;
}
}
步骤2:从尾->头,处理相同的后置元素
从end indedx
开始向前遍历,如果找到相同类型的元素,直接进行patch()
进行数据的更新,然后递减e1
和e2
const patchKeyedChildren = (c1, c2, container, ...args) => {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1; // prev ending index
let e2 = c2.length - 1; // next ending index
// 步骤1:从头->尾,patch()处理相同的前置元素,递增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];
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, null, ...args1);
} else {
break;
}
e1--;
e2--;
}
}
步骤3:旧children
已经处理完毕,但是新children
还有元素,处理新增元素,直接进行mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
从上面代码块可以知道新增元素有两种情况,都满足i>e1 && i<=e2
:
- 一种是新增元素在末尾,那么
patch()
传入parentAnchor
作为参照,将新增元素添加到container
子元素的末尾 - 一种是新增元素在头部,那么
patch()
传入c2[e2+1].el
,即上面示例元素a
所对应的el
作为参照,将新增元素添加到元素a
的前面
const patchKeyedChildren = (c1, c2, container, ...args) => {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1; // prev ending index
let e2 = c2.length - 1; // next ending index
// 步骤1:从头->尾,patch()处理相同的前置元素,递增i
// 步骤2:从尾->头,patch()处理相同的后置元素,递减e1和e2
// 3. common sequence + mount
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], container, anchor, ...args1);
i++;
}
}
}
}
步骤4:旧children
还有剩余,但是新children
已经处理完毕,处理已经废弃元素,直接进行unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
从上面代码块可以知道有两种情况,都满足i>e2 && i<=e1
:
- 一种是废弃元素在末尾,直接调用
unmount()
进行[i, e1]
的元素清除 - 一种是废弃元素在头部,直接调用
unmount()
进行[i, e1]
的元素清除
const patchKeyedChildren = (c1, c2, container, ...args) => {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1; // prev ending index
let e2 = c2.length - 1; // next ending index
// 步骤1:从头->尾,patch()处理相同的前置元素,递增i
// 步骤2:从尾->头,patch()处理相同的后置元素,递减e1和e2
if (i > e1) {
// 步骤3:旧children已经处理完毕,但是新children还有元素,处理新增元素,直接进行mount
} else if (i > e2) {
// 4. common sequence + unmount
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true);
i++;
}
}
}
步骤5:最复杂的情况,处理相同的前置元素+处理相同的后置元素后,剩下的元素有新增
、废弃
、乱序
的情况,需要复杂处理
从Vue 3.2.30
的源码可以知道,源码中对步骤5也进行了核心步骤的注释,下面的解析也将围绕着源码注释的步骤而讲解,主要步骤为:
步骤5.1
:为新vnode的children
建立索引[key: vnode.key, value: index]
,这样就可以通过key=vnode.key
快速找到对应的索引位置步骤5.2
:- 移除废弃的
旧vnode
- 使用
旧vnode
去找对应的新vnode
,进行vnode
的复用。如果找到可复用的vnode
,进行patch()
更新,同时使用newIndexToOldIndexMap[newIndex - s2] = i + 1
为新vnode
存储对应旧vnode
的index(所有index递增了1位,比如0变成1,1变成2)
- 使用
newIndex < maxNewIndexSoFar
判断该diff
是否需要移动节点
- 移除废弃的
步骤5.3
:移动/新增元素处理
const patchKeyedChildren = (c1, c2, container, ...args) => {
// 步骤1:从头->尾,patch()处理相同的前置元素,递增i
// 步骤2:从尾->头,patch()处理相同的后置元素,递减e1和e2
if (i > e1) {
// 步骤3:旧children已经处理完毕,但是新children还有元素,处理新增元素,直接进行mount
} else if (i > e2) {
// 步骤4:旧children还有剩余,但是新children已经处理完毕,处理已经废弃元素,直接进行unmount
} else {
// 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
// 5.1 build key:index map for newChildren
// 5.2 loop through old children left to be patched and try to patch
// 5.3 move and mount
}
}
步骤5.1:为newChildren
建立索引
const s1 = i; // prev starting index
const s2 = i; // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap = new Map();
for (i = s2; i <= e2; i++) {
const nextChild = c2[i];
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i);
}
}
步骤5.2:移除废弃的旧vnode
+ 更新能复用的旧vnode
+ newIndexToOldIndexMap
和move
的构建为下一步骤做准备
初始化变量
let j;
let patched = 0;
const toBePatched = e2 - s2 + 1;
let moved = false;
let maxNewIndexSoFar = 0;
const newIndexToOldIndexMap = new Array(toBePatched);
for (i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0;
逻辑1:移除废弃的旧vnode
如果prevChild.key
缺失,会进行[s2, e2]
的所有newChild
遍历,使用isSameVNodeType
去找可以复用的vnode节点
,如果还找不到对应的newIndex
,那就说明是废弃vnode
了
- 情况1:
newIndex === undefined
代表新vnode
已经找不到目前的旧vnode
,说明旧vnode
已经废弃,直接unmount()
移除 - 情况2:
patched >= toBePatched
代表新vnode
已经循环完成,不需要再找复用的旧vnode
,说明剩下还没复用的旧vnode
都已经废弃,直接unmount()
移除,一般发生在旧vnode
末尾存在废弃节点
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) { // 情况2
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])) {
newIndex = j;
break;
}
}
}
if (newIndex === undefined) {// 情况1
unmount(prevChild, parentComponent, parentSuspense, true);
} else {
patched++;
}
}
逻辑2:更新能复用的旧vnode
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) {
// 逻辑1:移除废弃的旧vnode的情况1
unmount(prevChild, parentComponent, parentSuspense, true);
continue;
}
let newIndex;
// -------- 寻找能够复用的vnode的newIndex --------------
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key);
} else {
//...如果prevChild.key缺失,会进行[s2, e2]的所有newChild遍历,使用isSameVNodeType去找可以复用的vnode节点
}
// -------- 寻找能够复用的vnode的newIndex --------------
if (newIndex === undefined) {
// 逻辑1:移除废弃的旧vnode的情况2
unmount(prevChild, parentComponent, parentSuspense, true);
} else {
// 逻辑2:更新能复用的旧vnode
patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
patched++;
}
}
逻辑3:newIndexToOldIndexMap
和move
的构建为步骤5.3
做准备
move
的构建:用旧vnode
进行遍历,去找新vnode
对应的index
,使用maxNewIndexSoFar
不断更新找到新vnode
对应的index
,如果newIndex < maxNewIndexSoFar
说明目前的旧vnode
的位置应该移动到它前面旧vnode
的前面去,即目前的旧vnode
是需要移动的,此时将move=true
newIndexToOldIndexMap
的构建:用新vnode
的index(从0开始)
作为key
,用旧vnode
的index+1(原来oldChildrenArray的index+1,不是从0开始)
作为value
,构建一个Array对象
,为步骤5.3
做准备
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
if (patched >= toBePatched) {
// ...省略代码 逻辑1:移除废弃的旧vnode的情况1
continue;
}
let newIndex;
// ...省略代码:寻找能够复用的vnode的newIndex
if (newIndex === undefined) {
// ...省略代码 逻辑1:移除废弃的旧vnode的情况2
} else {
// ========逻辑3:newIndexToOldIndexMap和move的构建为下一步骤做准备========
newIndexToOldIndexMap[newIndex - s2] = i + 1;
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
// ========逻辑3:newIndexToOldIndexMap和move的构建为下一步骤做准备========
// ...省略代码 逻辑2:更新能复用的旧vnode
}
}
步骤5.3:移动/新增处理
由步骤5.2
可以知道,该步骤会移除所有废弃的旧vnode
,因此剩余的逻辑只有 移动可复用的vnode
到正确的位置 + 插入之前没有过的新vnode
新增:判断目前新vnode
是之前没有过的新vnode
由步骤5.2 逻辑3
可以知道,newIndexToOldIndexMap
是以newChildrenArray
建立的初始化都为0的数组,然后在步骤5.2 逻辑3
进行oldChildrenArray
的遍历,找到可以复用的新vnode
,进行newIndexToOldIndexMap[xxx]=旧vnode的index+1
的赋值,因此newIndexToOldIndexMap[xxx]=0
就代表这个新vnode
是之前没有过的,直接进行patch(null, nextChild)
,即mount()
插入新的元素
// 从尾->头进行新vnode的遍历
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, isSVG, slotScopeIds, optimized);
} else if (moved) {
}
}
移动:判断目前新vnode
对应的可复用的旧vnode
是否需要移动位置
流程1: 构建最长递增子序列
Vue3源码关于最长递增子序列算法的构建可以参考leetcode高赞题解(推导流程详细),在本文中将直接抛弃算法细节,直接使用最长递增子序列的构建方法getSequence()
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR;
j = increasingNewIndexSequence.length - 1;
increasingNewIndexSequence
:使用newIndexToOldIndexMap
数组构建最长递增子序列,如下图所示,我们有newIndexToOldIndexMap
=[4, 1, 0, 5, 3]
,最长递增子序列的意思为item
是保持递增的一个序列,即[index=1,index=5]
/[index=1,index=3]
等等都是递增的值,我们通过getSequence(newIndexToOldIndexMap)
获取到的值为newChildren
中对应的递增子序列的index
数组,通过手动触发getSequence()
,我们得到increasingNewIndexSequence=[1,4]
我们初始化newIndexToOldIndexMap
时,每一个item
都赋值为0,获取oldChildren
的index
都主动加上了1,因此newIndexToOldIndexMap[xxx]=0
代表是之前旧children没有过的vnode,不参与构建最长递增子序列的逻辑中
流程2: 根据increasingNewIndexSequence进行节点的移动
使用倒序遍历新的children
数组是因为anchor
,如果没有相同的后置元素,则添加到container
子元素的末尾,如果有相同的后置元素,则以最前面一个后置元素作为anchor
patch
流程中会触发n2.el=n1.el
,move()方法
本质还是拿到了新vnode对应的el
,然后进行hostInsert(el, container, anchor)
移动操作
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR;
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, isSVG, slotScopeIds, optimized);
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2 /* REORDER */);
} else {
j--;
}
}
}
const move = (vnode, container, anchor, moveType, parentSuspense = null) => {
const { el, type, transition, children, shapeFlag } = vnode;
//...多种类型的数据进行不同的move处理,包括组件、Comment、Static
if (needTransition) {
//...
} else {
hostInsert(el, container, anchor);
}
};
- 如果
increasingNewIndexSequence.length=0
,即构建不出来最长递增子序列的时候,按照上面代码块的逻辑,我们从末尾开始,使用当前新vnode
后面的index
作为参照,不停将旧vnode
移动插入到c2[nextIndex+1]
的前面 - 如果
increasingNewIndexSequence.length>0
,那么遇到i === increasingNewIndexSequence[j]
时,代表目前的nextChild
是最长递增子序列的一个元素,由于最长递增子序列代表旧vnode
的相关位置在新vnode
的相关位置仍然保持递增,因此这些位于最长递增子序列的元素可以不进行move
操作,直接进行j--
即可
increasingNewIndexSequence
最长递增子序列的作用:获取旧的children在新的children的相对位置顺序仍然递增的最长子序列,减少move的次数,提升性能