17.2.1 带有 v-if 指令的节点
dynamicChildren收集动态节点时忽略虚拟 DOM 的层级,而v-if/v-else会让模板结构在更新前后不稳定,只让根节点当 Block 会直接产生更新 bug。
一、问题:动态节点集合相同,更新被跳过
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<div v-else>
<p>{{ a }}</p>
</div>
</div>
假设只有最外层的 div 会作为 Block 角色,无论 foo 是 true 还是 false,Block 收集到的动态节点都是同一个 p:
// foo 为 true → 渲染 section;foo 为 false → 渲染 div
// 但两种情况收集到的动态节点完全相同
const block = {
tag: 'div',
dynamicChildren: [{ tag: 'p', children: ctx.a, patchFlags: PatchFlags.TEXT }],
}
- 更新前后
dynamicChildren不变 ⇒ Diff 阶段不会做任何更新; - 可是
<section>与<div>是两个不同的标签,标签都变了却不更新,必然产生严重 bug。
二、标签相同也会失败
<div>
<section v-if="foo">
<p>{{ a }}</p>
</section>
<section v-else> <!-- 即使这里也是 section -->
<div> <!-- 这个 div 在 Diff 过程中被忽略 -->
<p>{{ a }}</p>
</div>
</section>
</div>
两个分支的标签都是 section,但内部虚拟 DOM 树的结构不同:一个子节点是 p,另一个是 div > p。由于 dynamicChildren 里只收集到了 p,div 这层被忽略,结构变化同样无法被察觉。
根本原因:
dynamicChildren数组中收集的动态节点忽略虚拟 DOM 树层级,即结构化指令会让更新前后模板结构发生变化——模板结构不稳定。
三、解法:让结构化指令的节点自己成为 Block
需要让带 v-if / v-else-if / v-else 这类结构化指令的节点也作为 Block 角色,这样 Block 树变成:
Block(div)
├── Block(section v-if)
└── Block(section v-else)
两个子 Block 作为父级 Block 的动态节点,被收集进父级 Block 的 dynamicChildren:
const block = {
tag: 'div',
dynamicChildren: [
// Block(section v-if) 或者 Block(section v-else)
{
tag: 'section',
key: 0, // key 值随不同分支变化
dynamicChildren: [{ tag: 'p', children: ctx.a, patchFlags: PatchFlags.TEXT }],
},
],
}
foo为真时,父 Block 的dynamicChildren中是Block(section v-if);foo为假时,变成Block(section v-else);- Diff 时渲染器能依据 key 判断更新前后的两个 Block 不同,用新的 Block 替换旧的 Block,而不是错位更新。
带
v-if/v-else的 Block 必须带key:没有 key,渲染器无法区分「同一个 Block 的内容变了」还是「换成了另一个 Block」。
相关
- 17.1.4 渲染器的运行时支持:
patchBlockChildren依赖 dynamicChildren 的结构稳定 - 17.2.2 带有 v-for 指令的节点:另一种让结构不稳定的指令