小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
什么是 diff 算法呢?简单总结就是:diff 算法是一种对比算法,即对比新旧 VNodes 的过程(看看哪里需要变化,再去变化哪里)。对比过程中,实现在尽量不变动旧 VNodes 的前提下完成新 VNodes 的生成。
那么 diff 算法该如何实现呢?其实,上一篇文章我们已经讲了 3 种方案,性能有高有低。那么 Vue 是采用什么方案实现 diff 算法的呢?
事实上,Vue 是根据你有没有传入 key 来决定 diff 算法采用的方案的:
- 有
key时,采用方案3,执行patchKeyedChildren()方法; - 没有
key时,采用方案2,执行patchUnkeyedChildren()方法;
有关源码如下:
我们先来看没有 key 的情况,拿前面的代码为例:
<ul>
<li v-for="item in letters">{{ item }}</li>
</ul>
这时 Vue 会调用 patchUnkeyedChildren() 方法:
图解如下:
我们可以发现,上面的 diff 算法效率并不高:
C和D其实并不需要有任何改动;- 但由于
C被F使用了,导致后面所有的内容都要进行一次改动,并且最后再新增D。
再来看有 key 的情况,在前面的代码中添加 key 的绑定(这里因为没有 id,假设 item 是唯一的,就用 item 作为 key 值了):
<ul>
<li v-for="item in letters" :key="item">{{ item }}</li>
</ul>
这时 Vue 会调用 patchKeyedChildren() 方法:
// 此方法在文件 packages/runtime-core/src/renderer.ts 中的 baseCreateRenderer 函数中
const patchKeyedChildren = (
c1: VNode[], // 旧 VNodes,['A', 'B', 'C', 'D']
c2: VNodeArrayChildren, // 新 VNodes,['A', 'B', 'F', 'C', 'D']
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
// 旧 VNodes 中最后一个 VNode 的位置(索引)
let e1 = c1.length - 1 // prev ending index
// 新 VNodes 中最后一个 VNode 的位置(索引)
let e2 = l2 - 1 // next ending index
// 1. sync from start
// 从头部开始同步遍历新旧 VNodes
// (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)) { // 如果新旧 VNode 相同(type 相同并且 key 也相同)
// 就进行 patch
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else { // 否则
// 就跳出循环
break
}
i++
}
// 2. sync from end
// 从尾部开始同步遍历新旧 VNodes
// 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)) { // 如果新旧 VNode 相同(type 相同并且 key 也相同)
// 就进行 patch
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else { // 否则
// 就跳出循环
break
}
e1--
e2--
}
// 3. common sequence + mount
// 如果旧的 VNodes 遍历完了,新的 VNodes 还有剩余的,那就添加(挂载)这些剩余的新节点
// (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 的第 1 个参数传入 null 时,后续会进行挂载操作
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
// 如果新的 VNodes 遍历完了,旧的 VNodes 还有剩余的,那就移除(卸载)这些剩余的旧节点
// (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
// 如果是未知的节点序列(即中间存在不知道如何排列的位置序列),
// 则用 key 建立 map 索引图,
// 尽可能地从旧的 VNodes 中匹配新的 VNodes 中的 VNode(即最大限度地使用旧节点),然后移除旧的 VNodes 中剩余的 VNodes,
// 之后是移动节点和挂载新节点
// [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
// 根据 key 建立 map 索引图
const keyToNewIndexMap: Map<string | number, 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
// 拿到最长递增子序列进行 move 和 mount
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--
}
}
}
}
}
核心步骤如下:
图解如下:
- 从头部开始遍历比较:
- 新旧节点相同时(
type相同并且key也相同),继续比较; C和F的key不一样,跳出循环;
- 新旧节点相同时(
- 从尾部开始遍历比较:
- 新旧节点相同时(
type相同并且key也相同),继续比较; B和F的key不一样,跳出循环;
- 新旧节点相同时(
- 如果旧节点遍历完了,还有多余的新节点,那么就新增这些新节点:
- 如果新节点遍历完了,还有多余的旧节点,那么就移除这些旧节点:
- 最后一种情况就是未知的节点序列了,就是中间存在乱序的节点:
所以,我们可以发现,Vue 在进行 diff 算法的时候,只要有 key,就会尽量利用 key 来进行优化操作:
- 在没有
key的时候,diff算法效率较低; - 在进行插入或重置顺序的时候,保持相同的
key可以让diff算法更高效;
现在,再回过头去看 关于 key 属性作用的官方解释,是不是就有更加清晰了呢?
总之,实际开发中,在使用 v-for 时,我们一般会加上 key 属性,并给 key 属性绑定上某个唯一的值(比如 id),这样在进行 diff 算法时,性能会更高。