1 背景
了解框架的部分核心功能的实现是有必要的,不仅可以加深对框架的理解,在应用方面更加得心应手。其中包含了一些思想也可以在平时的学习工作中用到。
2 diff算法源码分析
vue3的diff算法的核心代码在runtime-core下面的render.ts文件中。这里使用了2个函数来处理不同的情况:patchKeyedChildren处理有key的情况,一般来说v-for中设置了唯一key的节点就会执行这个逻辑;patchUnkeyedChildren处理没有key的情况。
2.1 参数
简单讲一下参数
c1: VNode[]:旧的 VNode 数组。c2: VNodeArrayChildren:新的 VNode 数组。container: RendererElement:要将 VNode 渲染到的容器元素。anchor: RendererNode | null:anchor参数可以是一个节点对象,表示新元素将插入到该节点之前。如果anchor参数为null,则表示新元素将直接插入到容器的最后。parentComponent: ComponentInternalInstance | null:父组件的内部实例,可以为 null。parentSuspense: SuspenseBoundary | null:父级 Suspense 边界,可以为 null。isSVG: boolean:一个布尔值,表示是否渲染为 SVG 元素。slotScopeIds: string[] | null:作用域插槽的 ID 数组,可以为 null。optimized: boolean:是否启用了优化模式。为true时,函数会对新的 VNode 进行克隆操作,为false时,会修改原来的节点。
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
3 没有key的情况:patchUnkeyedChildren
可以参考这张图的流程,比较简单,就是在共同长度对比,如果一样就复用。然后在超出部分的话,就删除或者增加
const patchUnkeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
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
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
let i
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
)
}
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
)
}
}
4 存在key的情况patchKeyedChildren
大致的处理流程可以看这幅图,前面的四个步骤比较容易理解:
在步骤1、2中比较新旧节点数组的头部、尾部;如果其中有一个数组被遍历完了,就执行步骤3、4。
比较难理解的是步骤5.1-5.3的代码。
- 在5.1中根据新的节点数组
c2的key和节点位置构建了keyToNewIndexMap,在后面会用来做新旧节点的匹配; - 在5.2中创建数组
newIndexToOldIndexMap,用于记录哪些新节点可以复用旧节点; - 在5.2用
moved变量判断可复用的旧节点的顺序是否会变化; - 在5.2中卸载未被使用到的旧节点;
- 在5.3中,如果
moved为真,也就是节点顺序有变化,就先生成最长稳定子序列,这个序列中的结点代表可以原地复用 - 在5.3中,根据
newIndexToOldIndexMap在对应的位置挂载新的节点 - 在5.3中,根据最长稳定子序列,使用
move函数来移动乱序的节点
// 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
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
let j
// patched是计数器
let patched = 0
// 结束指针
const toBePatched = e2 - s2 + 1
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
// 创建数组,长度是新节点数组的剩余长度,toBePatched,初始数字0,0代表是新增节点,1代表可复用旧节点
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) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
// 获取newIndex
let newIndex
// 如果旧节点存在key
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
// 不存在key的话,就一个个遍历
} else {
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
// 代表旧节点没有被复用
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
// 旧节点被复用
} else {
// 数组对应项置1,代表c2数组中的对应节点已经找到了可以复用的旧节点
newIndexToOldIndexMap[newIndex - s2] = i + 1
// 更新 maxNewIndexSoFar,如果有旧数组中能复用的节点是乱序的moved 就设置为 true
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
// 如果需要moved,就需要生成最长稳定子序列
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
// toBePatched是
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) {
// mount new 新增节点
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
// 如果不是新的节点并且被移动过了,就需要move
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
步骤1-4的代码如下所示
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 // 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] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
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
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,
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
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
}
5 更多
在vue3中,diff算法的源码相比于vue2又进行了优化。
vue2是一个双端对比的diff算法,也就是还会对比旧节点数组的头和新节点数组的尾、旧节点数组的尾和新节点数组的头。
在后面节点的处理中,vue2 是通过对旧节点列表建立一个 { key, oldVnode }的映射表,然后遍历新节点列表的剩余节点,根据newVnode.key在旧映射表中寻找可复用的节点,然后打补丁并且移动到正确的位置。 vue3 则是建立一个存储新节点数组中的剩余节点在旧节点数组上的索引的映射关系数组,建立完成这个数组后也即找到了可复用的节点,然后通过这个数组计算得到最长递增子序列,这个序列中的节点保持不动,然后将新节点数组中的剩余节点移动到正确的位置