Vue 3 的“靶向更新”通过编译阶段构建 Block Tree 实现:Block 作为更新边界,自动识别动态节点并挂载 dynamicChildren 数组;配合 Patch Flag 查表式更新,跳过静态内容与冗余比对,使 diff 复杂度降至 O(m)。

Vue 3 实现“靶向更新”的关键,不是靠运行时猜哪些节点变了,而是在编译阶段就画好一张“动态地图”——Block Tree。它不改变模板写法,也不要求开发者手动标记,而是由编译器自动识别、分块、挂载信息,让 diff 过程跳过所有静态内容,只处理真正可能变化的节点。
Block 是更新边界,不是普通节点
一个节点只要满足以下任一条件,就会被编译器标记为 Block:
- 绑定动态属性(如
:class、:style、v-bind) - 使用流程控制指令(如
v-if、v-for、v-show) - 包含插值表达式或动态子节点(如
{{ msg }}、<p>{{ text }}</p>)
这个 Block 不是 DOM 节点本身,而是一个“更新作用域”:它包裹自身及所有后代中需要响应变化的部分,并在运行时挂载 dynamicChildren 数组,只存直系动态子节点(带 patch flag 的那些),不递归、不嵌套、不收静态提升节点。
两级跳过:从“全树扫描”变成“清单执行”
传统 diff 必须自顶向下遍历整棵 VNode 树,哪怕只改一个数字,标题、页脚、图标这些完全不变的内容也得走一遍比对逻辑。Block Tree 把这个过程拆成两层跳过:
-
第一层跳过:静态祖先根本不出现在 render 函数里——像
<h1>欢迎页</h1>这类纯静态节点,被静态提升(hoist)为常量,在组件初始化时创建一次,后续直接复用,连进 diff 的机会都没有 -
第二层跳过:进入 Block 后也不遍历全部 children——patch 过程拿到 Block 节点后,直接读取它的
dynamicChildren数组,只对其中的节点做更新,其余子节点(包括静态子节点和已提升节点)一律跳过
例如:<div v-if="show">
<h2>标题</h2>
<p>{{ msg }}</p>
</div> 中,<div> 是 Block,<code><h2></h2> 被提升,<p></p> 进入 dynamicChildren。后续更新只检查 <p></p>,<h2></h2> 完全不参与任何比对。
Patch Flag + Block Tree = 查表式更新
Block 提供“范围”,Patch Flag 提供“动作”。编译器给每个动态节点打上标志,比如:
-
1:仅文本内容变化 → diff 时只比对textContent,不查 class、style、子节点 -
2:class 变化 → 只更新 class 属性,跳过其他 -
8:props 变化 → 只比对 props,不触发子节点 diff
运行时遇到带 flag 的节点,Vue 不再做通用判断,而是按 flag 查表执行对应更新逻辑。整个 Block 内部的 diff 退化为一维数组遍历,复杂度从 O(n) 降至 O(m),m 是动态节点数,与模板总大小无关。
v-once 是 Static Block 的特例
v-once 节点不会生成普通 Block,而是被编译为带 PatchFlags.STATIC 的独立 static block。它在首次渲染后:
- 不参与响应式依赖收集(内部表达式如
{{ msg }}不再被追踪) - 后续 re-render 时,diff 直接跳过整个 block,复用上次创建的 DOM
- 不走任何 props 更新、子节点 diff 或指令逻辑
它不是“永远静态”,而是“单次渲染 + 持久复用”,靠的是 Block 结构 + 静态标记的协同,而非额外树形管理。

















