17.2.2 带有 v-for 指令的节点
v-for渲染节点数量可变,会让dynamicChildren的元素数量与对应关系错乱;解法是把v-for的结果用 Fragment 类型的 Block 表达。
一、问题:元素个数可变导致错位
<div>
<p v-for="item in list">{{ item }}</p>
<i>{{ foo }}</i>
<i>{{ bar }}</i>
</div>
设 list 由 [1, 2] 变为 [1],仍假设只有根节点 div 是 Block:
// 更新前
const prevBlock = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: 1, patchFlags: PatchFlags.TEXT },
{ tag: 'p', children: 2, patchFlags: PatchFlags.TEXT },
{ tag: 'i', children: ctx.foo, patchFlags: PatchFlags.TEXT },
{ tag: 'i', children: ctx.bar, patchFlags: PatchFlags.TEXT },
],
}
// 更新后
const nextBlock = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: item, patchFlags: PatchFlags.TEXT },
{ tag: 'i', children: ctx.foo, patchFlags: PatchFlags.TEXT },
{ tag: 'i', children: ctx.bar, patchFlags: PatchFlags.TEXT },
],
}
dynamicChildren只是扁平数组,patchBlockChildren按下标一一对应地 patch;- 更新后数组长度少了一个元素,于是
p(2)与i(foo)对比、i(foo)与i(bar)对比——错位更新。
传统 Diff 有一个非常重要的前置条件:参与 Diff 的节点必须是同层级节点;而
dynamicChildren里的节点未必是同层级的,所以不能直接按下标比对。
二、解法:v-for 的节点也作为 Block
v-for 指令渲染的是一个片段,因此用类型为 Fragment 的节点来表达它的渲染结果,并让该 Fragment 作为 Block 角色:
const block = {
tag: 'div',
dynamicChildren: [
// 这是一个 Block:v-for 渲染出的 Fragment
{ tag: Fragment, dynamicChildren: [/* v-for 的节点 */] },
{ tag: 'i', children: ctx.foo, patchFlags: PatchFlags.TEXT },
{ tag: 'i', children: ctx.bar, patchFlags: PatchFlags.TEXT },
],
}
这样一来,v-for 内部节点数量如何增减,都只影响子 Block 内部的 Diff,父级 dynamicChildren 的元素个数保持稳定。
相关
- 17.2.1 带有 v-if 指令的节点:同为结构化指令导致结构不稳定的问题
- 17.2.3 Fragment 的稳定性:Fragment 自身仍可能不稳定,如何处理