Appearance
Block Tree 与动态节点收集
PatchFlags 解决了“比什么”的问题,但没解决“比哪些节点”的问题。 传统 Diff 仍需遍历整棵树找到动态节点。
Block Tree 将动态节点收集到扁平数组,直接遍历。 这是一个非常精妙的设计,将复杂度从 O(节点总数) 降为 O(动态节点数)。
问题场景
html
<div>
<header>
<nav>
<a :href="link">{{ title }}</a> <!-- 动态,嵌套很深 -->
</nav>
</header>
<main>
<p>静态内容</p>
<p>静态内容</p>
<p>{{ content }}</p> <!-- 动态 -->
</main>
<footer>
<span>静态</span>
</footer>
</div>传统 Diff:需要遍历整棵树,复杂度 O(节点总数)。
期望:直接访问动态节点,复杂度 O(动态节点数)。
Block 的数据结构
Block 是一个带有 dynamicChildren 数组的 VNode:
javascript
const blockVNode = {
type: 'div',
children: [
{ type: 'header', children: [...] },
{ type: 'main', children: [...] },
{ type: 'footer', children: [...] }
],
// 扁平化的动态节点
dynamicChildren: [
{ type: 'a', patchFlag: 9 /* TEXT, PROPS */ },
{ type: 'p', patchFlag: 1 /* TEXT */ }
]
}更新时只遍历 dynamicChildren,跳过整棵静态子树。
动态节点收集
使用栈结构收集动态节点:
javascript
const blockStack = []
let currentBlock = null
function openBlock(disableTracking = false) {
blockStack.push(currentBlock = disableTracking ? null : [])
}
function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
}创建元素时收集:
javascript
function createElementVNode(type, props, children, patchFlag) {
const vnode = {
type,
props,
children,
patchFlag,
dynamicChildren: null
}
// 有 patchFlag 且当前有活动 block,则收集
if (currentBlock !== null && patchFlag > 0) {
currentBlock.push(vnode)
}
return vnode
}创建 Block:
javascript
function createBlock(type, props, children, patchFlag) {
const vnode = createElementVNode(type, props, children, patchFlag)
// 附加收集到的动态节点
vnode.dynamicChildren = currentBlock
closeBlock()
// 如果还有父 block,将此 block 也收集进去
if (currentBlock) {
currentBlock.push(vnode)
}
return vnode
}编译输出
html
<div>
<span>静态</span>
<span>{{ msg }}</span>
</div>javascript
function render(_ctx) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "静态"),
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}执行流程:
openBlock()初始化空数组currentBlock = []- 创建第一个 span,无 patchFlag,不收集
- 创建第二个 span,patchFlag=1,收集到 currentBlock
createElementBlock创建 div,附加 dynamicChildren
最终 div 的 dynamicChildren = [第二个 span]。
Block 边界
不是所有元素都是 Block。只有结构型指令会创建新的 Block:
v-if/v-else-if/v-elsev-for- 组件根节点
为什么?
因为这些指令可能改变子节点的结构。如果子节点结构变了,扁平数组就不能直接按索引比对了。
html
<div>
<span v-if="show">A</span>
<span v-else>B</span>
</div>v-if 和 v-else 分别是不同的 Block,各自维护自己的 dynamicChildren。
嵌套 Block
html
<div>
<span>{{ msg }}</span>
<div v-if="show">
<span>{{ inner }}</span>
</div>
</div>结构:
- 外层 div 是 Block,dynamicChildren 包含第一个 span 和 v-if Block
- v-if Block 的 dynamicChildren 包含内层 span
javascript
{
type: 'div',
dynamicChildren: [
{ type: 'span', patchFlag: 1 }, // {{ msg }}
{
type: Fragment, // v-if
dynamicChildren: [
{ type: 'span', patchFlag: 1 } // {{ inner }}
]
}
]
}更新时:
- 遍历外层 dynamicChildren
- 遇到 Block,递归遍历其 dynamicChildren
patchBlockChildren
javascript
function patchBlockChildren(oldChildren, newChildren, parentEl) {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// 直接 patch,不需要遍历查找
patch(
oldVNode,
newVNode,
// 父元素从 oldVNode.el 获取
oldVNode.el.parentNode
)
}
}复杂度:O(dynamicChildren.length),而不是 O(整棵树)。
STABLE_FRAGMENT
如果 Fragment 的子节点结构稳定,标记 STABLE_FRAGMENT:
html
<template v-if="show">
<span>A</span>
<span>B</span>
</template>子节点数量和顺序固定,可以直接按索引比对。
KEYED_FRAGMENT vs UNKEYED_FRAGMENT
v-for 的 Fragment:
html
<div v-for="item in list" :key="item.id">{{ item.name }}</div>有 key → KEYED_FRAGMENT,使用带 key 的 Diff 算法。
html
<div v-for="item in list">{{ item.name }}</div>无 key → UNKEYED_FRAGMENT,使用简单的 Diff 算法。
Block 的限制
结构不稳定时
html
<div>
<span v-if="show">A</span>
<span>B</span>
</div>如果 show 从 true 变为 false,dynamicChildren 的结构就变了。
解决:v-if 创建自己的 Block,与外层隔离。
组件边界
组件内部的 dynamicChildren 对外部不可见。每个组件是独立的 Block。
本章小结
本章分析了 Block Tree 的实现:
- Block 结构:带有 dynamicChildren 的 VNode
- 收集机制:openBlock、createBlock、栈结构
- Block 边界:v-if、v-for、组件根节点
- 更新优化:只遍历 dynamicChildren
Block Tree 将 Diff 复杂度从 O(树节点数) 降到 O(动态节点数)。下一章我们将分析事件缓存——避免不必要的子组件更新。