本文正在参加「金石计划 . 瓜分6万现金大奖」
在 vue2 或 vue3 的文档中,都可以看到这么一句话:
建议尽可能在使用 v-for 时提供 key attribute。
记得几年前的一次面试我就被问到过这么个问题 —— 为什么在使用 v-for 进行列表渲染时要提供 key 属性?我当时的回答是因为文档建议了,于是面试官建议我回去等通知。今天,我们就去看看 vue3 的源码,找寻文档为什么会这么建议的原因,下载或调试 vue3 源码的方法参见《将 vue3 源码下载到本地调试》。
当我们往一个数组中,插入一个新元素时,vue 会调用定义于 packages\runtime-core\src\renderer.ts 的 patchChildren()
方法,该方法会首先判断下我们是否有提供 key 属性,然后再调用不同的方法处理:
const patchChildren: PatchChildrenFn = (...) => {
...
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// 提供了 key 属性
patchKeyedChildren(...)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// 没有提供 key 属性
patchUnkeyedChildren(...)
return
}
}
有 key
如果提供了 key,也就是 PatchFlags.KEYED_FRAGMENT
为 true,则调用 patchKeyedChildren()
方法,方法里主要做了 5 件事,其源码也做了注释:
- 从头部开始遍历
// 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, ...)
} else {
break
}
i++
}
在一个 while 循环中,从旧 VNode 列表中取出 n1 节点,从新 VNode 列表中取出 n2 节点,然后传给 isSameVNodeType()
判断 n1 和 n2 的类型是否相同,该函数的返回值为:
n1.type === n2.type && n1.key === n2.key
由此可以理解官方文档中的:
key 特殊 attribute 主要用做 Vue 的虚拟 DOM 算法的提示,以在比对新旧节点组时辨识 VNodes。
如果发现新旧节点的类型与 key 值都相同,则将它们传给 patch()
进行 diff 处理;
如果发现它们的类型或 key 值不同则执行 break
终止循环。图示如下:
- 从尾部开始遍历
// 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, ...)
} else {
break
}
e1--
e2--
}
与第一步做的事情类似,只不过是从尾部开始遍历,直至遇到类型或 key 值不同的节点终止循环。
- 判断是否需要挂载新节点
// 3. common sequence + mount
if (i > e1) {
if (i <= e2) {
// ... 省略部分代码
while (i <= e2) {
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
...
)
i++
}
}
}
当前两步执行完毕,如果发现新节点数量多于旧节点,则将 null 作为第一个参数传给 patch
函数,进行挂载(mount)操作。图示如下:
- 判断是否需要卸载旧节点
// 4. common sequence + unmount
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
当前两步执行完毕,如果发现新节点数量少于旧节点,则进行卸载(unmount)操作。
- 处理如下面注释中那样未知的乱序排列
// 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 {...}
这种情况处理起来比较复杂,总结起来就是 vue 会借助 key 值,跟踪每个节点的身份,对现有元素进行重用和重新排序。
没有 key
如果没有提供 key,则调用 patchUnkeyedChildren()
方法,其部分源码如下所示:
const patchUnkeyedChildren = (c1: VNode[], c2: VNodeArrayChildren, ...) => {
// ... 省略部分代码
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, ...)
}
if (oldLength > newLength) {
// remove old
unmountChildren(c1, ...)
} else {
// mount new
mountChildren(c2, ...)
}
}
大致过程是先获取新旧节点数组的长度,然后用 Math.min()
取它们中较小的那个,作为 for 循环的判断依据;再从下标值为 0 开始,一个个依次从新旧节点数组取出 VNode 传给 patch()
进行 diff;
最后判断下新旧节点的数量,如果旧节点多就卸载,新节点多则创建并挂载。
可以看出,在不提供 key 值时,在一些情况下会使得 diff 效率比提供了 key 值的时候低很多 —— 比如下图这样往数组中间位置新增节点的情况:
明明 c 节点并没有改变,但是因为是按顺序依次 patch,所以 oldVnode 中的 c 节点会和 newVnode 中的 d 节点进行 patch,然后再新增一个节点 c。
本文正在参加「金石计划 . 瓜分6万现金大奖」
暂无评论内容