17.1.2 Block 与 PatchFlags
只要运行时能区分动态内容与静态内容,就能实现极致的优化:Block 负责把动态节点收集起来,PatchFlags 说明每个动态节点「哪里动态」。
一、从传统 vnode 到带标记的 vnode
传统虚拟 DOM 中,没有任何标志能体现出节点的动态性:
const vnode = {
tag: 'div',
children: [
{ tag: 'div', children: 'foo' },
{ tag: 'p', children: ctx.bar }, // 实际是动态节点,但结构上看不出来
],
}
编译优化后,编译器把关键信息「附着」到虚拟节点上,并把动态子节点提取进 dynamicChildren 数组:
const vnode = {
tag: 'div',
children: [
{ tag: 'div', children: 'foo' },
{ tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT }, // 动态节点
],
// 将 children 中的动态节点提取到 dynamicChildren 中
dynamicChildren: [
{ tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT },
],
}
二、Block 是什么
- 带有
dynamicChildren属性的虚拟节点称为 Block(块); - 一个 Block 不仅能收集它的直接动态子节点,还能收集所有动态子代节点(收集时忽略层级);
- 渲染器更新一个 Block 时,会忽略虚拟节点的
children,直接找到dynamicChildren并只更新其中的动态节点,从而跳过全部静态内容; - 动态节点带有补丁标志,所以更新时还能靶向更新:例如
patchFlag为PatchFlags.TEXT,就只更新文本内容; - 编写模板时,所有模板的根节点都会是一个 Block 节点。
<template>
<!-- 这个 div 是 Block:它是根节点 -->
<div>
<!-- 这个 p 不是 Block:它不是根节点 -->
<p>{{ bar }}</p>
</div>
<!-- 这个 h1 是 Block:它是根节点 -->
<h1>
<span :id="dynamicId"></span>
</h1>
</template>
Block 是「树」而不是扁平数组:Block 里还可以嵌套子 Block,形成 Block 树。
三、PatchFlags:把「哪里动态」编码为数字
const PatchFlags = {
TEXT: 1, // 动态的文本内容
CLASS: 1 << 1, // 动态的 class
STYLE: 1 << 2, // 动态的 style
PROPS: 1 << 3, // class/style 之外的动态 props
FULL_PROPS: 1 << 4, // 有动态 key 的 props,需要全量 props diff
HYDRATE_EVENTS: 1 << 5, // 带事件监听器的节点
STABLE_FRAGMENT: 1 << 6, // 子节点顺序不会改变的 Fragment
KEYED_FRAGMENT: 1 << 7, // 带 key 的 Fragment
UNKEYED_FRAGMENT: 1 << 8, // 不带 key 的 Fragment
NEED_PATCH: 1 << 9, // 只需非属性更新(ref、自定义指令等)
DYNAMIC_SLOTS: 1 << 10, // 动态插槽
HOISTED: -1, // 静态节点(用于静态提升)
BAIL: -2, // 退出优化模式
}
| 标志 | 值 | 含义 |
|---|---|---|
TEXT | 1 | 节点有动态的 textContent |
CLASS | 1 << 1 | 元素有动态的 class 绑定 |
STYLE | 1 << 2 | 元素有动态的 style 绑定 |
PROPS | 1 << 3 | 除 class/style 外的动态属性 |
FULL_PROPS | 1 << 4 | 带动态 key 的属性,需完整 props diff |
HYDRATE_EVENTS | 1 << 5 | 节点上挂着事件监听器 |
STABLE_FRAGMENT | 1 << 6 | 子节点顺序不变的 Fragment |
KEYED_FRAGMENT | 1 << 7 | 带 key 的 Fragment |
UNKEYED_FRAGMENT | 1 << 8 | 不带 key 的 Fragment |
NEED_PATCH | 1 << 9 | 只需非属性更新(如 ref、自定义指令) |
DYNAMIC_SLOTS | 1 << 10 | 动态插槽 |
HOISTED | -1 | 静态节点,用于静态提升 |
BAIL | -2 | 放弃优化,退回普通 Diff |
书中为便于理解,示例里用
TEXT: 1、CLASS: 2、STYLE: 3这种简化编号;真实实现是1 << n的位运算,两套编号不能混用。
四、为什么用位运算
- 一个节点可能同时有多个动态因素,用按位或组合即可:
PatchFlags.TEXT | PatchFlags.CLASS; - 渲染器用按位与检测:
patchFlag & PatchFlags.CLASS,一次运算就能判断某类更新是否需要执行,还能一次判断多个标志; - 一个数字即可表达任意组合,既省内存又是常数时间判断。
相关
- 17.1.1 传统Diff算法问题:需要这套标记机制的起因
- 17.1.3 收集动态节点:
dynamicChildren在编译器生成的渲染函数里如何被收集