编译 优化

文章目录

  • 前言
  • 一、编译流程概览
  • 二、静态提升(HoistStatic)
    • [2.1 原理](#2.1 原理)
    • [2.2 静态属性也会提升](#2.2 静态属性也会提升)
    • [2.3 收益](#2.3 收益)
  • [三、PatchFlag 动态标记](#三、PatchFlag 动态标记)
    • [3.1 原理](#3.1 原理)
    • [3.2 常见 PatchFlag 类型](#3.2 常见 PatchFlag 类型)
    • [3.3 对比 Vue 2](#3.3 对比 Vue 2)
  • [四、Block Tree](#四、Block Tree)
    • [4.1 问题](#4.1 问题)
    • [4.2 Block 与 dynamicChildren](#4.2 Block 与 dynamicChildren)
    • [4.3 Diff 时的行为](#4.3 Diff 时的行为)
    • [4.4 与 PatchFlag 配合](#4.4 与 PatchFlag 配合)
  • 五、缓存事件处理函数(cacheHandlers)
    • [5.1 问题](#5.1 问题)
    • [5.2 编译优化](#5.2 编译优化)
    • [5.3 限制](#5.3 限制)
  • 六、其他编译相关优化
    • [6.1 静态字符串提升](#6.1 静态字符串提升)
    • [6.2 v-once](#6.2 v-once)
    • [6.3 v-memo(Vue 3.2+)](#6.3 v-memo(Vue 3.2+))
  • [七、Vue 3 为什么更快(编译视角)](#七、Vue 3 为什么更快(编译视角))
  • 八、面试聚焦
    • [8.1 Vue 3 比 Vue 2 快的核心原因(编译层)](#8.1 Vue 3 比 Vue 2 快的核心原因(编译层))
    • [8.2 PatchFlag 有哪些类型?](#8.2 PatchFlag 有哪些类型?)
    • [8.3 静态提升能减少什么开销?](#8.3 静态提升能减少什么开销?)
    • [8.4 Block Tree 是什么?](#8.4 Block Tree 是什么?)
  • 九、易混淆点
  • 十、思考与练习
  • 总结

前言

Vue 3 的性能提升不仅来自 Proxy 响应式和快速 Diff,更来自编译时优化:在构建阶段分析模板,减少运行时 VNode 创建和 Diff 范围。本篇会讲清楚:

  • 静态提升(HoistStatic)
  • PatchFlag 动态标记
  • Block Tree 与事件缓存
  • Vue 3 比 Vue 2 快的编译层原因

一、编译流程概览

复制代码
.vue 模板
    ↓ parse(解析为 AST)
    ↓ transform(静态分析 + 优化标记)
    ↓ generate(生成 render 函数)
render 函数执行 → VNode → patch → DOM

核心思想:能在编译期确定的(静态结构、动态位点),就不放到运行时反复计算。

优化手段 编译期做什么 运行时收益
静态提升 静态 VNode 提到函数外 不重复创建
PatchFlag 标记哪里会变 Diff 只比动态位
Block Tree 收集 dynamicChildren 跳过静态子树
cacheHandlers 缓存内联事件函数 减少子组件无效更新

二、静态提升(HoistStatic)

2.1 原理

模板中完全静态 的节点(无绑定、无指令),编译时提升到 render 函数外部,整个应用生命周期只创建一次 VNode,后续 render 直接复用。

vue 复制代码
<template>
  <div>
    <h1>固定标题</h1>
    <p>静态段落</p>
    <p>{{ msg }}</p>
  </div>
</template>
javascript 复制代码
// 编译结果示意(简化)
const _hoisted_1 = createStaticVNode(`<h1>固定标题</h1>`, 1)
const _hoisted_2 = createStaticVNode(`<p>静态段落</p>`, 1)

function render(_ctx) {
  return createElementVNode('div', null, [
    _hoisted_1,   // 复用,不重新创建
    _hoisted_2,
    createElementVNode('p', null, _ctx.msg, PatchFlags.TEXT)
  ])
}

2.2 静态属性也会提升

vue 复制代码
<div class="container" id="main">
  <span>hello</span>
</div>

整个静态子树可一次性提升,包括静态 class、id 等。

2.3 收益

  • 避免每次 render 重新创建静态 VNode
  • 静态子树不参与后续 Diff
  • 大型模板中静态区域越多,收益越明显

三、PatchFlag 动态标记

3.1 原理

编译器分析模板,给含动态内容的 VNode 打上 patchFlag 位掩码,告诉运行时「只有这些部分可能变」。

vue 复制代码
<div :class="cls">{{ msg }}</div>
javascript 复制代码
createElementVNode(
  'div',
  { class: _ctx.cls },
  _ctx.msg,
  PatchFlags.TEXT | PatchFlags.CLASS  // 3
)

Diff 时只比较 TEXT 和 CLASS,不遍历整个 props 对象。

3.2 常见 PatchFlag 类型

名称 含义
1 TEXT 动态文本内容
2 CLASS 动态 class
4 STYLE 动态 style
8 PROPS 动态 props(非 class/style)
16 FULL_PROPS props 绑定对象可能整体替换
32 NEED_HYDRATION SSR 水合需要
128 KEYED_FRAGMENT 带 key 的 Fragment
256 UNKEYED_FRAGMENT 无 key 的 Fragment
512 NEED_PATCH 子节点需 Diff
1024 DYNAMIC_SLOTS 动态插槽
-1 HOISTED 静态提升节点
-2 BAIL 全量 Diff(优化失败回退)

多个标记用按位或 组合,如 TEXT | CLASS = 3

3.3 对比 Vue 2

Vue 2 运行时对 VNode 全量比较 props 和 children。Vue 3 有 patchFlag 时按位检查,跳过未标记部分,减少比较次数。

javascript 复制代码
// Vue 3 运行时示意
if (patchFlag & PatchFlags.TEXT) {
  // 只比文本
}
if (patchFlag & PatchFlags.CLASS) {
  // 只比 class
}
// patchFlag === 0 → 该节点完全静态,可跳过

四、Block Tree

4.1 问题

传统 VNode 树是嵌套结构,Diff 需递归整棵树。模板里往往大量静态节点包裹少量动态节点,全树递归浪费。

4.2 Block 与 dynamicChildren

编译器为每个「Block 根」收集动态子节点 到扁平数组 dynamicChildren

vue 复制代码
<template>
  <div>                          <!-- Block 根 -->
    <header>静态导航</header>
    <p>{{ title }}</p>           <!-- 动态 → dynamicChildren[0] -->
    <footer>静态页脚</footer>
    <span>{{ count }}</span>     <!-- 动态 → dynamicChildren[1] -->
  </div>
</template>
javascript 复制代码
// Block VNode 示意
{
  type: 'div',
  dynamicChildren: [
    pVNode,    // {{ title }}
    spanVNode  // {{ count }}
  ]
}

4.3 Diff 时的行为

更新时只遍历 dynamicChildren,静态的 header、footer 子树整段跳过,无需递归进入。

复制代码
传统:递归 div → header → p → footer → span(5 层)
Block:直接 patch dynamicChildren [p, span](2 个节点)

4.4 与 PatchFlag 配合

  • PatchFlag:单个节点内「哪些属性」动态
  • Block Tree:整棵子树里「哪些节点」动态

两者叠加,大幅缩小 Diff 范围。


五、缓存事件处理函数(cacheHandlers)

5.1 问题

vue 复制代码
<button @click="count++">+1</button>

每次 render 都会生成新的内联函数引用,子组件可能认为 props 变化而无效更新。

5.2 编译优化

编译器将稳定的事件处理器缓存到 render 函数上下文:

vue 复制代码
<button @click="handleClick">点击</button>
javascript 复制代码
// 编译结果示意
function render(_ctx, _cache) {
  return createElementVNode('button', {
    onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
  }, '点击')
}

同一 render 周期内复用 _cache[0],避免每次新建函数,减少子组件 patch。

5.3 限制

依赖外部变量且每次需新闭包的内联箭头函数,可能无法缓存或标记 BAIL 回退全量 Diff。


六、其他编译相关优化

6.1 静态字符串提升

动态绑定中的静态字符串 也会提升,如 class="fixed":class="dynamic" 共存时,静态部分不参与重复创建。

6.2 v-once

vue 复制代码
<header v-once>
  <h1>{{ title }}</h1>
</header>

运行时指令:该子树只渲染一次,后续更新跳过 Diff。与编译静态提升不同,v-once 是开发者手动标记。

6.3 v-memo(Vue 3.2+)

vue 复制代码
<div v-memo="[item.id, item.selected]">
  <ExpensiveItem :item="item" />
</div>

依赖数组不变则跳过该子树更新,适合大列表项细粒度优化。


七、Vue 3 为什么更快(编译视角)

维度 Vue 2 Vue 3
静态节点 每次 render 重建 VNode 静态提升,只建一次
Diff 范围 全树递归 Block + dynamicChildren
节点比较 全量 props/children PatchFlag 按位比较
事件函数 每次新建 cacheHandlers 缓存
列表 Diff 双端 Diff 快速 Diff + LIS

编译优化 + 运行时 Diff 优化共同作用,使 Vue 3 在典型业务模板下更新更快、内存更省。


八、面试聚焦

8.1 Vue 3 比 Vue 2 快的核心原因(编译层)

静态提升、PatchFlag、Block Tree、事件缓存,减少 VNode 创建量和 Diff 范围;配合 Proxy 响应式和快速 Diff。

8.2 PatchFlag 有哪些类型?

TEXT(1)、CLASS(2)、STYLE(4)、PROPS(8) 等,按位或组合;HOISTED(-1) 表示静态提升节点。

8.3 静态提升能减少什么开销?

避免每次 render 重建静态 VNode,静态子树不参与 Diff,降低 GC 压力。

8.4 Block Tree 是什么?

编译期收集动态节点到 dynamicChildren,Diff 只比扁平动态列表,跳过静态子树。


九、易混淆点

  1. 编译优化仅针对模板:手写 render 函数默认享受不到,需自己优化。
  2. PatchFlag 与 Block 互补:一个管节点内动态位,一个管树里哪些节点动态。
  3. 静态提升 ≠ v-once:前者编译自动识别;后者手动声明跳过更新。
  4. BAIL(-2):优化无法应用时回退全量 Diff,保证正确性。
  5. 优化基于模板静态分析:过于动态的模板优化效果有限。

十、思考与练习

1. 静态提升做了什么?

解析:编译期把静态 VNode 提到 render 外,只创建一次,后续复用,不参与 Diff。

2. PatchFlag 的作用?

解析:位掩码标记 VNode 哪些部分动态,Diff 时只比较标记位,跳过静态 props/children。

3. Block Tree 如何减少 Diff?

解析:dynamicChildren 扁平收集动态节点,更新时只 patch 这些节点,静态子树整段跳过。

4. cacheHandlers 解决什么问题?

解析:缓存内联事件函数引用,避免每次 render 新函数导致子组件误判 props 变化。

5. 开发者如何配合编译优化?

解析:模板保持结构清晰、静态与动态分离;大列表用稳定 key;静态大块可用 v-once;昂贵子树用 v-memo。


总结

  • 静态提升:静态 VNode 只创建一次,复用 + 跳过 Diff
  • PatchFlag:按位标记 TEXT/CLASS/STYLE/PROPS 等动态内容
  • Block Tree:dynamicChildren 扁平化,只 Diff 动态节点
  • cacheHandlers:缓存事件函数,减少无效更新
  • 本质:编译期分析模板,把运行时工作前移,是 Vue 3 性能的关键