上一章介绍了Block树的概念,最后说到当遇到不稳定Fragment的时候不会去使用动态子节点,而是会去使用children与老的vnode节点进行diff算法,我们上节说过使用v-for命令的节点会被Fragment包裹,所以进行更新patch时会执行processFragment,我们就从这个函数看起
processFragment
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// fragment前后锚点 用于标记fragment的开始和结束 是两个空的文本节点
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (
__DEV__ &&
// #5523 dev root fragment may inherit directives
(isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
) {
// HMR updated / Dev root fragment (w/ comments), force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
// check if this is a slot fragment with :slotted scope ids
// 检查是否是带有:slotted作用域id的插槽片段
if (fragmentSlotScopeIds) {
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds
}
if (n1 == null) {
// insertBefore操作
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
// 挂载子节点
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT && // 是否是稳定的Fragment
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// patchFlag是PatchFlags.STABLE_FRAGMENT(稳定的Fragment)且有dynamicChildren
// 直接遍历dynamicChildren更新
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
} else if (
// #2080 if the stable fragment has a key, it's a <template v-for> that may
// get moved around. Make sure all root level vnodes inherit el.
// #2134 or if it's a component root, it may also get moved around
// as the component is being moved.
n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)
) {
traverseStaticChildren(n1, n2, true /* shallow */)
}
} else {
// 不稳定的Fragment,没有dynamicChildren,直接使用children去进行diff算法
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
我们只看不稳定的情况patchChildren
patchChildren
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) { // 判断是否是带key的Fragment
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
// 对于有key的采用快速diff算法
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
// 对于没有key的采用非常简单的diff算法
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
// children has 3 possibilities: text, array or no children.
// 其他三种类型children的处理:文本、数组、没有子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
所以对于不稳定的Fragment一般会有两种情况,有key执行patchKeyedChildren,没有key执行patchUnkeyedChildren,我们先看简单的没有key的diff
没有key的diff算法:patchUnkeyedChildren
const patchUnkeyedChildren = (
c1: VNode[], // 旧vnode children
c2: VNodeArrayChildren, // 新vnode children
container: RendererElement, // 容器
anchor: RendererNode | null, // 锚点
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
// 记录新旧vnode children的长度
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
let i
// 遍历较短的那个children 直接按顺序patch
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
// 如果旧的节点比新的节点多,那么就把多余的节点删除 从commonLength开始
if (oldLength > newLength) {
// remove old
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength
)
} else { // 如果新的节点比旧的节点多,那么就把多余的节点挂载 从commonLength开始
// mount new
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
commonLength
)
}
}
可以看到没有key的diff算法非常简单,下面来看有key的diff算法,也是我们常说的快速diff算法:patchKeyedChildren
快速diff算法:patchKeyedChildren
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let i = 0
const l2 = c2.length // 记录新子节点长度
let e1 = c1.length - 1 // 老子节点结束索引
let e2 = l2 - 1 // 新子节点结束索引
// 1. sync from start
// (a b) c
// (a b) d e
// 从索引0开始比较,如果相同就更新,不同就跳出循环
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// n1.type === n2.type && n1.key === n2.key
// 如果vnode的type相同且key相同,则认为是同一个vnode 执行patch操作
if (isSameVNodeType(n1, n2)) {
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] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
e1--
e2--
}
// 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
// 如果i > e1 说明前面两步已经把所有旧节点都更新完了
// 此时只需要把还未处理的新节点挂载到容器中即可
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
// 挂载的锚点是e2的下一个节点 如果该节点没有则是parentAnchor(一般是null)
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,
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
// 如果i > e2 说明已经把所有新节点都更新完了 那么只需要卸载多余的旧节点即可
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
// 创建一个Map用来存储新子节点的key和索引的映射关系
// 为后续遍历旧子节点时能快速根据key找到对应的新子节点的索引做准备 防止过高的时间复杂度 属于空间换时间的做法
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
warn(
`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
// 5.2 遍历旧的子节点,尝试匹配新的子节点,匹配的到则patch更新,匹配不到则卸载节点
let j
let patched = 0
// 剩余未处理新节点的数量
const toBePatched = e2 - s2 + 1
// 是否需要进行dom的移动操作
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
// 声明一个newIndexToOldIndexMap数组
// 数组的索引是剩余新节点的索引(从0开始)
// 值是对应节点在所有旧节点中的位置(这里的位置是指在所有旧节点中排第几个index+1,避免0索引与默认值0冲突)
const newIndexToOldIndexMap = new Array(toBePatched)
// 使用0填充数组 0表示新节点没有对应的旧节点 最后需要执行创建节点的操作
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
// 所有的新节点都已经patch更新了 所以这里只能是卸载节点
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
// 如果旧节点缺少key 则遍历未处理新节点寻找相同类型的节点
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
// 如果没有获取到新节点的索引 则卸载prevChild
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 存在对应的新节点则更新 新节点索引和旧节点位置的映射关系
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
// 5.3 移动和挂载
// 只有dom需要移动时(moved为true)才生成最长递增子序列
// 根据newIndexToOldIndexMap获取最长递增子序列increasingNewIndexSequence
// 最长递增子序列是一个由newIndexToOldIndexMap索引填充的数组 数组中对应索引的新节点不用移动
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] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
// 为0表示是新节点 挂载新节点
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
// 判断i是否等于最长递增子序列的最后一个元素 如果不等于则移动节点 否则不移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
快速diff算法难理解的地方可能在于最长递增子序列,为什么最长递增子序列对应的新节点不需要移动?这其实很好理解,这里利用了相对位置的概念。试想一下一些节点他们之间的相对位置没有改变,那么他们是不是不需要移动?我们是不是只需要移动节点到正确的位置就可以了?所以我们要找出最长的相对位置没有发生改变的节点数组,以此来移动最少数量的节点,以节省性能。
PS:对于快速diff算法最好写一个简单的示例debugger一下比较好,因为其中一些对于索引的处理还是比较绕,debugger走一遍会让思路更加清晰。下面提供一个小示例,供大家调试:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<div v-for="item in arr" :key="item">{{ item }}</div>
<button @click="changeArr">change</button>
</div>
<script src="../../dist/vue.global.js"></script>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const arr = ref(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
const changeArr = () => {
arr.value = ['a', 'b', 'e', 'c', 'd', 'h', 'f', 'g']
}
return {
arr,
changeArr
}
}
}).mount('#app')
</script>
</body>
</html>