Vue 3 在编译阶段和运行时都做了大量工作来避免不必要的重复计算与 DOM 操作。核心思路是:尽可能早地识别出“不会变化”的部分,并在更新时直接跳过它们。
静态节点:编译时“打标签”
在模板编译的 parse 和 optimize 阶段,Vue 编译器会遍历整个模板 AST,并给每个节点标记是否“静态”——即它的内容、属性不会受任何响应式数据影响。
<div>
<h1>这是一个静态标题</h1>
<p>{{ message }}</p>
</div>
编译后的结果(简化示意):
<h1>被标记为static: true,它的 VNode 创建函数会被提升到渲染函数之外。<p>包含插值message,是动态节点,但它的动态属性会被标记为PatchFlag.TEXT(文本内容会变)。
静态提升:不重复造轮子
“静态提升”是编译优化中最显著的操作。静态节点的 VNode 生成逻辑被提取到渲染函数外部,成为全局常量:
// 编译后的渲染函数(简化版)
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "这是一个静态标题", -1 /* HOISTED */)
function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1, // 直接复用静态 VNode
_createElementVNode("p", null, _ctx.message, 1 /* TEXT */)
]))
}
实用价值:无论组件因为数据变化重渲染多少次,<h1> 的 VNode 对象都只会创建一次,后续渲染直接引用。这减少了内存分配和垃圾回收压力,对包含大量静态布局的页面(如后台表单、文章详情)效果明显。
PatchFlags:运行时“按需检查”
动态节点会被编译器注入 PatchFlags(补丁标记),它是一个位掩码,告诉运行时 Diff 算法“只需要检查我这个节点的哪一项属性”。
| PatchFlag 常量 | 含义 |
|----------------|------|
| 1 (TEXT) | 文本内容动态 |
| 2 (CLASS) | class 属性动态 |
| 4 (STYLE) | style 属性动态 |
| 8 (PROPS) | 属性(不含 class/style)动态 |
| 16 (FULL_PROPS) | 带 key 的动态属性,需要全量对比 |
| 32 (HYDRATE_EVENTS) | 事件需要重新绑定 |
| 64 (STABLE_FRAGMENT) | 子节点顺序不变,只做内容更新 |
| 128 (KEYED_FRAGMENT) | 带 key 的子节点片段 |
运行时 Diff 时,Vue 会读取 patchFlag,如果节点没有标记(即静态节点),则直接跳过整棵子树;如果有标记,则根据掩码只检查对应属性。这比传统的全量递归对比快得多。
直观感受:一个包含 100 个 <li> 的列表,如果只有其中某一项的文案更新,Vue 不会再次检查其他 99 项的 class、style 或事件绑定——因为 PatchFlags 告诉它“这里只有文本会变”。
Block 树:跳过静态子树
Vue 3 在编译时还会生成 Block 树。一个组件的模板被分割成若干“区块”(Block),每个 Block 只收集自己内部的动态后代节点。当组件更新时,只遍历 Block 树中收集的动态节点列表,所有静态子树都被整体跳过,无需深入递归。
这使得一个大型组件即使有复杂的嵌套结构,只要大部分是静态的,更新成本依然保持在极低水平。
运行时缓存机制:让重复计算“懒”下来
除了编译时的静态优化,Vue 还提供了一些机制来在运行时缓存计算结果,避免不必要的重复渲染。
1. 事件处理函数缓存(cacheHandlers)
默认情况下,模板中内联的事件处理函数会在每次渲染时重新创建:
<button @click="count++">+1</button>
这会导致子组件因为接收到新的 onClick 属性而触发无意义的重渲染。Vue 编译器可以开启 cacheHandlers 选项(Vue 3 默认开启),将这类内联函数自动包装为缓存版本:
// 编译后
createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = $event => ($data.count++))
})
这样即使父组件多次重渲染,传给按钮的事件处理函数引用保持不变,子组件不会因此更新。
2. v-once:只渲染一次
对于完全静态、只在初始化时绑定一次数据的区域,可以使用 v-once 指令:
<header v-once>
<h1>{{ siteTitle }}</h1>
</header>
即使 siteTitle 是响应式数据,这个 <header> 也只会在首次渲染后“冻结”为静态内容,后续任何数据变化都不会触发它的更新。适合用于页面标题、页脚等初始化后就不再改变的部分。
3. v-memo:手动指定依赖缓存
Vue 3.2+ 提供了 v-memo 指令,允许你明确告诉 Vue:“只有当这些依赖变化时,才重新渲染这段模板”:
<div v-memo="[list.length]">
<!-- 这里的复杂模板,只在 list.length 变化时才更新 -->
<ExpensiveComponent :items="list" />
</div>
如果 list 的内容改变了但长度没变,该 div 的子节点会被重用,跳过整个子树的 Diff。这在包含大表单、富文本编辑器等重量级组件的场景中能显著减少不必要的渲染。
实用总结
- 静态提升 + Block 树 让 Vue 默认就很快,你不需要额外配置。
v-once用于彻底固化的内容,v-memo用于手动优化复杂模板的更新条件。- 事件缓存 自动生效,帮你减少子组件因内联函数而产生的额外渲染。
这些机制共同保证了 Vue 应用在“数据驱动”的同时,不会因为“频繁重渲染”而浪费性能——它总是只更新真正变化的最小部分。