vue3diff算法
什么是vue3diff算法
Vue3中的diff算法是一种用于比较虚拟DOM树之间差异的算法,其目的是为了高效地更新真实DOM,减少不必要的重渲染
主要过程
整个过程主要分为以下五步
- 前置预处理
- 后置预处理
- 仅处理新增
- 仅处理后置
- 处理包含新增、卸载、移动的复杂场景
第一步:前置预处理
首先定义一个变量i,记录当前索引值
定义e1、e2记录前置索引值
从前到后遍历新旧索引列表
发现它们的节点值相同,则直接patch打补丁更新
否则跳出循环进入下一步。
vue3源码:
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++
}
以下demo为例:
i=0 新旧节点都是n1 patch打补丁更新
i=1 新旧节点都是n2 patch打补丁更新
i=2 新节点n3!== n2 跳出循环
第二步:后置预处理
从后到前去遍历新旧索引列表
发现它们的节点相同,则直接patch
打更新
否则跳出循环
并记录e1 e2的值
vue3源码:
// 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--
}
以下demo为例:
e1=6、e2=6对应都是n7节点,patch打补丁更新
e1=5、e2=5对应的新旧节点不同
跳出循环,记录e1、e2的值
第三步:处理仅有新增节点情况
当i > e1 并且i <= e2 :代表仅有新增节点
则直接patch打补丁更新
vue3源码:
// 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++
}
}
}
以下demo为例:
i = 2时,新旧节点不同,从后往前遍历
e1=2、e2=6对应都是n7节点,patch打补丁更新
e1=1、e2=5对应的新旧节点不同
这时i > e1 并且i <= e2,仅有新增节点,直接更新
第四步:处理仅有卸载节点情况
当i > e2 并且i <= e1 :代表仅有卸载节点
则直接卸载节点
vue3源码:
// 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++
}
}
以下demo为例:
i = 2时,新旧节点不同,从后往前遍历
e1=2、e2=6对应都是n7节点,patch打补丁更新
e1=5、e2=1对应的新旧节点不同
这时i > e2 并且i <= e1 ,仅有卸载节点,直接卸载
第五步:处理其他情况(新增、卸载、移动)
定义以下变量
变量 | 定义 |
---|---|
s1 | 记录旧节点列表要处理部分的起始位置 |
s2 | 记录新节点列表要处理部分的起始位置 |
keyToNewIndexMap | 新节点位置映射表;用于映射新节点与位置的映射关系 |
newIndexToOldIndexMap | 新旧节点位置映射表;用于记录新旧节点位置的映射关系。初始值为0,如果都是0,则判定为新节点,需要挂载 |
maxNewIndexSoFar | 当前最远位置;用于记录新节点中当前的最远位置。目的是用于判断遍历过程中是否呈现递增趋势。如果不是则代表产生了移动 |
moved | 递减趋势,需要移动则标识为true;后续进行移动处理 |
j | 最长递增子序列位置 |
以下demo为列:
变动点:卸载n3 / 新增n8 / 移动n6
当s1=2 : n3 ;在新节点位置映射表内没有找到;则为卸载,把该节点移除
当s1=3: n4; 在新节点位置映射表中可以找到;则将s1 + 1 = 4放在新旧节点位置映射表中。同时对比新节点位置索引和最远位置,3 > 0,则将新索引位置记录到最远位置中。最后打补丁更新
当s1=4: n5; 在新节点位置映射表中可以找到;则将s1 + 1 = 5放在新旧节点位置映射表中。同时对比新节点位置索引和最远位置,4 > 3,则将新索引位置记录到最远位置中。最后打补丁更新
当s1=5: n6; 在新节点位置映射表中可以找到;则将s1 + 1 = 6放在新旧节点位置映射表中。同时对比新节点位置索引和最远位置,2 > 4,呈现递减趋势,说明位置发生了移动,则移动标识为true。最后patch打补丁更新
到这一步已经遍历完旧节点。这时候需要根据映射表找到最长递增子序列
目的是为了让节点做最小限度的移动操作
从下图中新旧节点映射表中可以看出:最长递增子序列索引值是4/5,将其记录下来,对应的就是1/2
从后开始往前遍历新旧节点映射表
定义变量 i 记录当前新旧节点映射表位置,
定义变量 j 记录最长递增子序列位置,初始化为1
当i=3:0 对应n8节点。0代表新增,挂载该节点
当i=2: 5 对应n5节点。j=1 对应最长递增子序列。因此无需移动,直接跳转
当i=1: 4 对应n4节点。j=2对应最长递增子序列中。因此无需移动,直接跳转
当i=0: 6对应n6节点。不处于最长递增子序列当中。因此需要移动,执行移动操作
这样下来,整个过程只需要挂载n8节点、卸载n3节点、移动n6节点,其他全部原地patch更新。性能得到了极大的保证
vue3源码:
// 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<PropertyKey, 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--
}
}
}
}
以上就是vue3 DOM DIFF算法的整个过程
vue3源码地址: https://github.com/vuejs/core
render文件:vue3\core\packages\runtime-core\src\renderer.ts