知识补充
patchFlag
- 有编译器产生的用于优化的提示。可以理解为对虚拟节点VNode的属性进行描述的标志。
- 在Vue3中,会用一个数字来对虚拟节点VNode的属性进行标记,采用按位与(&)运算来确定节点的属性。例如VNode是否有动态的文本({{text}})、动态的Class(:class=“xxx”)和是否为携带有key值的循环片段等。
常见值
TEXT = 1,
CLASS = 1 << 1,
STYLE = 1 << 2,
PROPS = 1 << 3,
// 带key值的循环片段
KEYED_FRAGMENT = 1 << 7,
// 不带key值得循环片段
UNKEYED_FRAGMENT = 1 << 8,
通过位与来判断
- 假设patchFlag是135(二进制10000111),KEYED_FRAGMENT = 1 << 7(二进制10000000),位与的结果是10000000即128,说明该VNode为携带key值的循环判断。同理,可以得到该片段还具备动态的class、text和style。
const patchFlag = 0b10000111,
KEYED_FRAGMENT = 1 << 7
/**
* 10000111
* 10000000
* 位与后的结果
* 10000000
*/
console.log(patchFlag & KEYED_FRAGMENT) // 128
核心方法
源码文件路径:core-main\packages\runtime-core\src\renderer.ts
isSameVNodeType
- 根据节点的类型和key值来快速判断两个VNode是否相同
function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
patchChildren
- 用来比对更新子节点,当节点的标记(patchFlag)为KEYED_FRAGMENT或UNKEYED_FRAGMENT则会分别调用patchKeyedChildren和patchUnkeyedChildren进行比对。
const patchChildren = (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 & 128 /* 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, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
return;
}
else if (patchFlag & 256 /* PatchFlags.UNKEYED_FRAGMENT */) {
// unkeyed
patchUnkeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
return;
}
}
...
}
patchKeyedChildren
- 携带key值的情况下,新旧子节点列表的比对,利用到了diff算法。
实现过程
- 旧子节点列表为c1,新子节点列表为c2。
- c1的边界为e1(c1.length - 1),c2的边界为e2(c2.length - 1)
- 头头比对(i++):从i=0开始遍历c1、c2,调用isSameVNodeType比对新旧节点,相同则调用patch打补丁后继续比对。
// 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++
}
- 尾尾比对(e1- -、e2- -):从两者的边界e1、e2从尾往头比对,同理,直到尾部不相同为止。
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
e1--
e2--
}
- 判断当前未遍历的情况,由1、2步可以保证当前c1、c2未遍历的数组头尾项都不相同。
-
- 情况一:
i > e1 && i <= e2
,即c1旧节点列表遍历完了,c2新节点列表还未遍历完。此时c2剩余未遍历的节点,即全部都是新节点,直接继续遍历通过patch打补丁完成挂载(mount)即可。
- 情况一:
-
- 情况二:
i <= e1 && i > e2
,此时c2新节点列表已经遍历完成,但c1未遍历完,即旧节点列表c1里的所有节点都是没用的,直接继续遍历卸载掉(unmounted)c1剩余节点即可。
- 情况二:
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++
}
}
}
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
-
- 其他情况:c1和c2都未遍历完,需要进一步对比。
两个未知顺序数组的比对
- 假设
c1=["a", "b", "c", "d"]
,c2=["a","e","b","d"]
。经过以上头头、尾尾比对后,当前剩余为遍历为["b", "c"] ["e", "b"]
,显然旧节点列表c1中的"b"是可以复用的。 - 具体过程如下:
- 遍历c2剩余的项,生成由key到index的映射keyToNewIndexMap。
-
- 记住我们的例子生成了
keyToNewIndexMap {'e' => 0, 'b' => 1}
- 记住我们的例子生成了
// 简单例子
const c2 = ["e", "b"]
const mapC2 = new Map()
// mapC2 {'e' => 0, 'b' => 1}
for (let i = 0; i < c2.length; i++) mapC2.set(c2[i], i)
// 源码
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]
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
- 根据c2剩余未遍历部分的长度生成一个数组newIndexToOldIndexMap并填满0。
-
- 第1步生成了c2 key到index的映射
keyToNewIndexMap {'e' => 0, 'b' => 1}
- 第1步生成了c2 key到index的映射
-
- 当前例子生成
newIndexToOldIndexMap=[0,0]
。
- 当前例子生成
-
- 解释newIndexToOldIndexMap的意义。
-
-
- 例如
newIndexToOldIndexMap=[0,1],旧c1=["b", "c"],新c2=["e", "b"]
。
- 例如
-
-
-
newIndexToOldIndexMap[0]为0
即我是c2剩余未遍历节点[“e”, “b”]的第0项"e",我在遍历旧节点列表c1 [“b”, “c”]的过程中,利用keyToNewIndexMap传入key值没有找到跟你一样key的,所以你的值还是初始值0。
-
-
-
newIndexToOldIndexMap[1]为1
即我是c2 [“e”, “b”]的第1项"b",我在遍历旧节点列表c1 [“b”, “c”]时,同理利用keyToNewIndexMap.get(“b”) // 找到1,即发现了c1第1项也是"b"。你俩key值相同,可以复用。我记录下你俩的位置关系。
-
-
-
-
newIndexToOldIndexMap[c2中"b"的位置索引] = 在c1中找到的"b"的位置索引 + 1
。
-
-
-
-
-
newIndexToOldIndexMap[1] = 0 + 1
。
-
-
-
- 总结:newIndexToOldIndexMap的索引index即c2当前项的位置,value为c2项在c1中的位置加1。如果当前位置的值为0,则c2该位置的节点是全新的节点,直接生成即可 。否则,通过移动来实现复用。
let j
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
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
- 遍历c1剩余的项。
-
- 利用
c1[s1]
的key值,在keyToNewIndexMap中找c2同key值项的位置赋值给newIndex,找到则完善newIndexToOldIndexMap,否则unmount卸载掉当前项。
- 利用
// 简要代码
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
let newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
-
- 当已比对的项patched已经满足c2未遍历长度toBePatched时(
patched >= toBePatched
),卸载掉多余的项。
- 当已比对的项patched已经满足c2未遍历长度toBePatched时(
if (patched >= toBePatched) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
-
- 具体源码:
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
}
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] as VNode)
) {
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(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
- 移动和挂载新节点。
完整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 // 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. 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
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
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
}
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] as VNode)
) {
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(
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
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
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
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
patchUnkeyedChildren
- 用于比对两个没有添加key的新旧Vnode列表。
- 步骤:
- 获取旧节点列表c1和新节点列表c2的长度,再取其最小长度(防止遍历的时候越界)
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
- 头头比对,一个个patch,相同则复用,不同则直接打补丁、重新生成再替换。
let i
for (i = 0; i < commonLength; i++) {
const nextChild = c2[i]
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
- 如果c2遍历完,c1未遍历完,即旧节点列表未遍历完,卸载掉多余的节点。相反,新节点列表未遍历完,则生成新的节点。
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
)
}
完整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
)
}
}