1、patchChildren
前面我们提到了存在children的vnode,那么存在children就需要patch每一个children vnode向下遍历,我们这里就需要用到patchChildren,依次patch子类的vnode。
vue3.0中有这么一段源码
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
/* 对于存在key的情况用于diff算法 */
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
/* 对于不存在key的情况,直接patch */
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
return
}
}
patchChildren会根据key是否存在进行真正的diff或者直接patch
那么我们看既然diff存在patchChildren方法中,patchChildren方法用在Fragment类型和element类型的vnode中,这样就解释了diff算法的作用域。
diff算法的作用
存在children的vnode,需要通过patchChildren遍历children依次进行patch操作,如果在patch期间再发下children,那么会再次以递归的方式向下patch,因此找到新旧vnode很重要。
这里我们可以设想下,如果不存在diff,依次按照先后顺序进行patch会怎么样?
如果没有diff算法,如果稍微调整下DOM结构,就不会有新老节点的复用,就需要重新创建新的vnode节点,这样浪费了很大的性能开销。
diff作用就是在patch子vnode过程中,找到与新vnode对应的老vnode,复用真实的dom节点,避免不必要的性能开销
2、diff算法做了什么?
上面的源码中我们提到了key与没有key,源码中有不同的方法patchKeyedChildren和patchUnkeyedChildren
首先我们看看不存在key
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++) { /* 依次遍历新老vnode进行patch */
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
if (oldLength > newLength) { /* 老vnode 数量大于新的vnode,删除多余的节点 */
unmountChildren(c1, parentComponent, parentSuspense, true, commonLength)
} else { /* /* 老vnode 数量小于于新的vnode,创造新的即可 */
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
commonLength
)
}
以上得出结论:
(1)比较新老children的length获取最小值 然后对于公共部分,进行从新patch工作;
(2)如果老节点数量大于新的节点数量 ,删除多出来的节点;
(3)如果新的节点数量大于老节点的数量,从新 mountChildren新增的节点(也就是创建新的节点)。
下面是有key的方法 patchKeyedChildren
/* 从头对比找到有相同的节点 patch ,发现不同,立即跳出*/
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
/* 判断key ,type是否相等 */
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
parentAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
break
}
i++
}
以上得出结论
(1)从头开始寻找相同的vnode,然后patch,如果发现不相同就跳出循环。
// isSameVNodeType
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
isSameVNodeType 作用就是判断当前vnode类型 和 vnode的 key是否相等
(2)从尾开始向前diff
如果第一步没有patch完,就从尾开始向前diff,发现相同就patch,不同立即跳出循环
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
parentAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
break
}
e1--
e2--
}
(3)看看老节点是否全部被patch,新节点没有被patch完,创建新的vnode,
/* 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode ) */
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch( /* 创建新的节点*/
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG
)
i++
}
}
}
//如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode ),也就是要全部create新的vnode.
(4)如果新节点全部被patch完毕,老节点仍有剩余,那么卸载所有超出的老节点。
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
//针对老节点多余新节点,老节点全部被卸载的情况说明已经patch完相同的vnode。
(5)不确定的元素(这说明没有patch完所有的相同的vnode)
然后我们回到第一步跟第二步,看看剩余节点怎么操作如下:
const s1 = i //第一步遍历到的index
const s2 = i
const keyToNewIndexMap: Map<string | number, number> = new Map()
/* 把没有比较过的新的vnode节点,通过map保存 */
for (i = s2; i <= e2; i++) {
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
let j
let patched = 0
const toBePatched = e2 - s2 + 1 /* 没有经过 path 新的节点的数量 */
let moved = false /* 证明是否 */
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
/* 建立一个数组,每个子元素都是0 [ 0, 0, 0, 0, 0, 0, ] */
遍历所有新节点把索引和对应的key,存入map keyToNewIndexMap中,keyToNewIndexMap 存放 key -> index 的map
接下来声明一个新的指针,记录剩下新节点的索引,我们可以看到这个patched记录新节点过的数量。
toBePatched记录第五步之前没有patched的新节点的数量
moved代表是否发生过移动
newIndexToOldIndexMap 用来存放新节点索引和老节点索引的数组
newIndexToOldIndexMap 数组的index是新vnode的索引 , value是老vnode的索引
然后我们看下核心代码
for (i = s1; i <= e1; i++) { /* 开始遍历老节点 */
const prevChild = c1[i]
if (patched >= toBePatched) { /* 已经patch数量大于等于, */
/* ① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点 */
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
/* ② 如果,老节点的key存在 ,通过key找到对应的index */
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else { /* ③ 如果,老节点的key不存在 */
for (j = s2; j <= e2; j++) { /* 遍历剩下的所有新节点 */
if (
newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新节点没有被patch */
isSameVNodeType(prevChild, c2[j] as VNode)
) { /* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex */
newIndex = j
break
}
}
}
if (newIndex === undefined) { /* ①没有找到与老节点对应的新节点,删除当前节点,卸载所有的节点 */
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
/* ②把老节点的索引,记录在存放新节点的数组中, */
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
/* 证明有节点已经移动了 */
moved = true
}
/* 找到新的节点进行patch节点 */
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
optimized
)
patched++
}
}
① 通过老节点的key找到对应新节点的index:开始遍历老的节点,判断有没有key, 如果存在key通过新节点的keyToNewIndexMap找到与新节点index,如果不存在key那么会遍历剩下来的新节点试图找到对应index。
②如果存在index证明有对应的老节点,那么直接复用老节点进行patch,没有找到与老节点对应的新节点,删除当前老节点。
③newIndexToOldIndexMap找到对应新老节点关系
我们patch一遍,将所有的老的vnode都patch了一遍。
接下来,我们孙然已经patch可一遍老节点,但是对于发生移动的节点,怎么真正的moved DOM节点。
对于新增的节点又该怎么处理呢?接着往下看,下面这个方法我不是很明白getSequence
/*移动老节点创建新节点*/
/* 根据最长稳定序列移动相对应的节点 */
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] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) { /* 没有老的节点与新的节点对应,则创建一个新的vnode */
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG
)
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) { /*如果没有在长*/
/* 需要移动的vnode */
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
(6)说是通过getSequence的到一个最长稳定序列。对应index==0的操作也就是新增节点,需要从新mount一个新的节点,对于发生移动的节点进行一个统一的移动操作。
*最长稳定序列,不是很懂,我还得研究下
那么我们为啥要得到这个一个东东,因为我们需要他作为一个参照的序列,其他不在列的节点进行移动。
以下总结是copy的,组织语言有点麻烦,省事,如有雷同,勿喷
总结如下:
①从头对比找到有相同的节点 patch ,发现不同,立即跳出。
②如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环。
③如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode )。
④ 对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )。
⑤不确定的元素( 这种情况说明没有patch完相同的vnode ) 与 3 ,4对立关系。
把没有比较过的新的vnode节点,通过map保存
记录已经patch的新节点的数量 patched
没有经过 path 新的节点的数量 toBePatched
建立一个数组newIndexToOldIndexMap,每个子元素都是[ 0, 0, 0, 0, 0, 0, ] 里面的数字记录老节点的索引 ,数组索引就是新节点的索引
开始遍历老节点
① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点
② 如果,老节点的key存在 ,通过key找到对应的index
③ 如果,老节点的key不存在
1 遍历剩下的所有新节点
2 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex
④ 没有找到与老节点对应的新节点,卸载当前老节点。
⑤ 如果找到与老节点对应的新节点,把老节点的索引,记录在存放新节点的数组中,
1 如果节点发生移动 记录已经移动了
2 patch新老节点 找到新的节点进行patch节点
遍历结束
如果发生移动
① 根据 newIndexToOldIndexMap 新老节点索引列表找到最长稳定序列
② 对于 newIndexToOldIndexMap -item =0 证明不存在老节点 ,从新形成新的vnode
③ 对于发生移动的节点进行移动处理。