Diff
前文讲述的都是单个节点的更新,单个节点的更新中,
patchElement最后一步为patchChildren
判断两个节点的孩子节点,这里面就包含了vue-Diff算法的体现
状态标记
patchFlag
export const enum PatchFlags {
// 动态文字内容
TEXT = 1,
// 动态 class
CLASS = 1 << 1,
// 动态样式
STYLE = 1 << 2,
// 动态 props
PROPS = 1 << 3,
// 有动态的key,也就是说props对象的key不是确定的
FULL_PROPS = 1 << 4,
// 合并事件
HYDRATE_EVENTS = 1 << 5,
// children 顺序确定的 fragment
STABLE_FRAGMENT = 1 << 6,
// children中有带有key的节点的fragment
KEYED_FRAGMENT = 1 << 7,
// 没有key的children的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 只有非props需要patch的,比如`ref`
NEED_PATCH = 1 << 9,
// 动态的插槽
DYNAMIC_SLOTS = 1 << 10,
// SPECIAL FLAGS -------------------------------------------------------------
// 以下是特殊的flag,不会在优化中被用到,是内置的特殊flag
// 表示他是静态节点,他的内容永远不会改变,对于hydrate的过程中,不会需要再对其子节点进行diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2,
}
为什么要这样子设置捏, 因为转化成二进制 只有 0010000 & 0010000 = 0010000才是 true, 0010000 & 0000001 = 0000000 就是 false,从而在diff算法判断入口
patchChildren()
packages/runtime-core/src/renderer.ts
面试经常会被提及的问题,详细分析
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
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
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// 新子节点为不带key的fragment 入口
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
}
}
// children has 3 possibilities: text, array or no children.
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 新子节点为文本
// text children fast path
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧子节点为数组,直接消除子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,
namespace,
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,
namespace,
slotScopeIds,
optimized,
)
}
}
}
}
patchKeyedChildren()
两个子节点都是数组,最复杂的情况,先贴完整代码
其中分为五步
- 自前向后
- 自后向前
- 新节点多于旧节点
- 旧节点多与新节点
- 乱序比对
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
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,
namespace,
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,
namespace,
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,
namespace,
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,
namespace,
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,
namespace,
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--
}
}
}
}
}
变量定义
let i = 0const l2 = c2.length新子节点数组长度 (length2)let e1 = c1.length - 1旧节点尾部索引 (end1)let e2 = c2.length - 1新节点尾部索引function isSameVNodeType(n1, n2)两个节点类型和key都相同
1. 自前向后
// 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,
namespace,
slotScopeIds,
optimized,
)
} else {
break
}
i++
}
- 如果两个节点是相同节点,直接patch这两个节点 (递归调用)[先是
pactch再patchchildren] - 直到这两个节点不是相同节点
- 至此,头部相同的元素被比对完了
此时i指向 首个不同的子节点
2. 自后向前
// 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,
namespace,
slotScopeIds,
optimized,
)
} else {
break
}
e1--
e2--
}
- 两个尾指针从后往前遍历
- 至此,尾部相同的元素被比对完了
此时,i指向首个不同的节点, [e1, e2] 指向尾端首个不同节点
3. 乱序比对-diff核心 ★
vue源码的注释把3.1 和 3.2 的情景列为4 和 5, 但我感觉他是3的特俗案例,所以用3.1 和 3.2 来表示啦,不喜欢勿喷,直接看源码注释好了
3.1 新节点数大于旧节点数-前面或后面完全相同
处理push 和 unshift 情景
// 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,
namespace,
slotScopeIds,
optimized,
)
i++
}
}
}
经过去除首尾处理后,只留下中间不同部分
- 都是一样的处理, 多出来的节点都执行
patch(null, c2[i]) - 如果是尾结点, anthor使用的是父节点的anchor
3.2 旧节点多与新节点-前面或后面完全相同
处理数组pop, shift情形
// 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++
}
}
没啥好说的,直接全部删掉好了
3.3 完全乱序情景 ★
一般就是两个完全不同数组的情况了
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,
namespace,
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,
namespace,
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--
}
}
}
}
3.3.1 给新子节点创建map
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) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
注意哦,i在第一步提后了,不是从头节点开始,e1,e2在第二步的时候提前了,就是头尾肯定是不同的了
3.3.2 遍历旧节点,尝试打补丁或者删除节点
// 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 // 尾index-头index+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,
namespace,
slotScopeIds,
optimized,
)
patched++
}
}
该逻辑主要是遍历旧节点,尝试打补丁或者删除节点。
这里 j 我们暂时先不管;patched 记录已经修复的新节点数量,比如当前还剩三个节点未修复,默认是 0;toBePatched 是新节点待修复数量,当前为 3;moved 为标记,代表当前节点是否需要移动;maxNewIndexSoFar 是配合 moved 是否有节点移动,保存着最大 newIndex 值;newIndexToOldIndexMap 为数组,它的下标表示新节点的下标,元素表示旧节点的下标,Vue 通过数组形式来表示 map <newIndex,oldIndex> 中数据。
另外还需注意的是,当前数组的下标是不计算已经处理好的节点。
3.3.3 主要进行移动和挂载的处理,需要配合最长递增子序列
// 用最大的连续递增序列,判断哪些节点不需要移动
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,
namespace,
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--
}
}
}
根据newIndexToOldIndexMap数组的值来判断(value为旧节点下标),如果是0,代表要新增,如果为1则移动,争取移动节点最少量
至此,这就完成了整个的diff算法
3.3.4 最长递增子序列
used for determining longest stable subsequence 获取最长递增子序列
旧节点:1, 2, 3, 4, 5, 6
新节点:1, 3, 2, 4, 6, 5
如果保留 1,3,6 就要变动3个节点, 如果保留1,2,4,6就只需要变动两个节点,所有要使用最长递增子序列
function increaseMaxSeq(arr) {
if (!arr.length) return [];
const len = arr.length;
let seq = [arr[0]];
for (let i = 1; i < len; i++) {
const target = arr[i];
if (target > seq[seq.length - 1]) {
seq.push(target);
continue;
} else {
const idx = biSearch(seq, target);
if (target === seq[idx - 1]) continue;
seq[idx] = target;
}
}
return seq;
}
function biSearch(arr, target) {
const len = arr.length;
let left = 0;
let right = len - 1;
while (left <= right) {
// 结束的时候right 在 left 左边
const mid = Math.floor((left + right) / 2);
if (arr[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 这个二分查找返回的index, 是大于或等于查找目标的下一个index
return left;
// 如果正常的二分查找应该是这样
// return target === arr[right] ? right : -1;
}
increaseMaxSeq([4,10,4,3,8,9]);
刷题时做的最长递增子序列算法, 力扣第300题, 中等题哈, 空间复杂度为 nlog(n)
总结
回顾一下整个diff算法的步骤
- 自前向后,判断头部节点是否相同
- 自后向前,判断尾部节点是否相同
- diff
- 只有尾部不同,新比旧多, 直接mount新节点(push),旧比新多,直接unmount(pop)
- 只有头部不同,旧比新多,直接mount新节点(unshift),新比旧多,直接mount(shift)
- 乱序排序
- 建立新节点的key,index的keyToNewIndexMap(Map)
- 遍历旧节点,看有没有能复用的,生成一个newIndexToOldIndexMap(Array),如果没有相同的直接删掉
- 根据newIndexToOldIndexMap并使用最长递增子序列算法,移动最少的子节点,旧的有就移动,没有就新增
参考: