Vue 编译器在优化阶段有一项关键任务:检测并缓存静态节点 。
这能显著减少渲染时的重复计算与 diff 操作,从而提高运行性能。
本文将完整剖析 Vue 源码中实现静态缓存的核心逻辑:cacheStatic()。
一、背景与设计目标
Vue 的编译过程分为三个主要阶段:
- 解析(parse) :将模板转成 AST 抽象语法树。
- 转换(transform) :在 AST 层面进行各种优化和重写。
- 生成(generate) :把优化后的 AST 输出为渲染函数(
render())。
cacheStatic() 属于第二阶段的"优化变换(transform)"阶段,其目标是:
- 识别静态节点(constant node) ;
- 提升(hoist)或缓存(cache)这些节点的渲染结果;
- 避免重复创建 VNode 实例,提高运行效率。
二、入口函数:cacheStatic()
javascript
export function cacheStatic(root: RootNode, context: TransformContext): void {
walk(
root,
undefined,
context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
!!getSingleElementRoot(root),
)
}
🧩 核心说明:
- 从根节点
root开始递归扫描整个 AST。 - 若根节点只有一个
<div>或<MyComp>,通过getSingleElementRoot()检查是否可提升。 - 最终调用内部递归函数
walk(),逐层分析每个节点是否可以缓存。
三、单根节点检测:getSingleElementRoot()
ini
export function getSingleElementRoot(root: RootNode) {
const children = root.children.filter(x => x.type !== NodeTypes.COMMENT)
return children.length === 1 &&
children[0].type === NodeTypes.ELEMENT &&
!isSlotOutlet(children[0])
? children[0]
: null
}
💬 功能说明:
- Vue 允许模板中多个根元素,但静态优化只在单根结构中有效。
- 过滤掉注释节点;
- 若只有一个普通元素且不是
<slot>,则返回该节点; - 否则返回
null。
四、核心递归:walk()
walk() 是整个静态分析的主干逻辑,递归遍历所有子节点并决定:
- 哪些节点可提升;
- 哪些节点可缓存;
- 哪些节点仍需运行时更新。
代码摘录(核心结构):
ini
function walk(node, parent, context, doNotHoistNode = false, inFor = false) {
const { children } = node
const toCache = []
for (const child of children) {
if (child.type === NodeTypes.ELEMENT) {
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_CACHE) {
(child.codegenNode as VNodeCall).patchFlag = PatchFlags.CACHED
toCache.push(child)
continue
}
} else {
// props 可提升,但节点自身可能含动态子节点
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = codegenNode.patchFlag
if (
(flag === undefined || flag === PatchFlags.NEED_PATCH) &&
getGeneratedPropsConstantType(child, context) >= ConstantTypes.CAN_CACHE
) {
const props = getNodeProps(child)
if (props) codegenNode.props = context.hoist(props)
}
}
}
}
}
// 缓存数组化的节点
if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
node.codegenNode.children = getCacheExpression(
createArrayExpression(node.codegenNode.children)
)
} else {
for (const child of toCache) {
child.codegenNode = context.cache(child.codegenNode!)
}
}
}
🧠 逐步拆解逻辑:
1. 判断是否为普通元素
- 仅对
ELEMENT和TEXT_CALL节点考虑缓存; - 组件或
v-for等结构指令不会被提升(需要响应式更新)。
2. 检查常量类型(getConstantType)
- 若节点完全静态(如纯文本、固定属性的
<div>),可标记为 可缓存(CAN_CACHE) ; - 若节点部分动态(如有绑定属性),则仅提升其属性对象。
3. 标记补丁标识(patchFlag)
- 将静态节点标记为
PatchFlags.CACHED; - 让运行时
patch()函数跳过更新。
4. Hoisting Props(属性提升)
-
若节点本身不可缓存,但其属性对象(
props)为常量,则调用:scsscontext.hoist(props)将其移动到函数外部作用域,避免重复创建。
5. 子节点缓存优化
-
若一个元素的全部子节点都是静态的,则整体缓存为数组表达式:
ininode.codegenNode.children = getCacheExpression(createArrayExpression(...)) -
否则,仅缓存部分节点。
五、静态判定核心:getConstantType()
这是 Vue 判断节点是否可缓存的"大脑"。
kotlin
export function getConstantType(node, context): ConstantTypes {
switch (node.type) {
case NodeTypes.ELEMENT:
if (node.tagType !== ElementTypes.ELEMENT) return ConstantTypes.NOT_CONSTANT
const cached = context.constantCache.get(node)
if (cached !== undefined) return cached
const codegenNode = node.codegenNode!
if (codegenNode.patchFlag === undefined) {
// 节点无 patchFlag,可能为静态节点
let returnType = ConstantTypes.CAN_STRINGIFY
const propsType = getGeneratedPropsConstantType(node, context)
if (propsType === ConstantTypes.NOT_CONSTANT) return propsType
// 检查子节点是否静态
for (const child of node.children) {
const childType = getConstantType(child, context)
if (childType === ConstantTypes.NOT_CONSTANT) return childType
}
context.constantCache.set(node, returnType)
return returnType
} else {
context.constantCache.set(node, ConstantTypes.NOT_CONSTANT)
return ConstantTypes.NOT_CONSTANT
}
}
}
🔍 工作原理:
-
利用缓存表(
constantCache)避免重复判断; -
若节点无动态补丁标志 (
patchFlag),进一步判断:- props 是否常量;
- 子节点是否常量;
-
若均为静态,则返回
CAN_STRINGIFY或更高级别的常量类型。
六、辅助逻辑:getGeneratedPropsConstantType()
该函数专门检测节点属性是否静态。
kotlin
function getGeneratedPropsConstantType(node, context) {
const props = getNodeProps(node)
if (props && props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
for (const { key, value } of props.properties) {
if (getConstantType(key, context) === ConstantTypes.NOT_CONSTANT) return 0
if (getConstantType(value, context) === ConstantTypes.NOT_CONSTANT) return 0
}
}
return ConstantTypes.CAN_STRINGIFY
}
核心思路:
- 递归检测 props 的
key与value; - 若任何一项动态(如绑定表达式
:class="foo"),则视为非静态; - 否则,可视为完全静态,允许 hoist。
七、总结:优化策略矩阵
| 场景 | 优化方式 | 举例 |
|---|---|---|
| 完全静态节点 | 整体缓存或 hoist | <div>hello</div> |
| 静态属性 + 动态子节点 | 提升 props | <div class="a">{{ msg }}</div> |
| 动态节点 | 不缓存 | <div :id="foo"></div> |
| 静态插槽内容 | 缓存数组表达式 | <slot><p>static</p></slot> |
八、拓展与启发
Vue 的静态提升机制体现了三个工程哲学:
-
编译时确定尽可能多的信息
在编译阶段完成常量折叠与缓存标记,减少运行时负担。
-
多级常量类型系统(
ConstantTypes)不只是"静态/动态"二分法,而是细化为:
CAN_STRINGIFYCAN_CACHECAN_SKIP_PATCHNOT_CONSTANT
-
AST 层级的细粒度优化
通过递归分析 props、children、helper 调用,最大化可缓存区域。
九、潜在问题与注意点
- v-for / v-if 限制:循环与条件节点必须保留为 block,不可 hoist;
- SVG/MathML 特殊节点:需保留 block 结构;
- 内存泄漏风险:Vue 在缓存数组时使用「展开式缓存」以避免 DOM 引用残留;
- HMR 热更新:缓存节点可能导致热替换不生效,因此在开发环境下做了额外防护。
✅ 结语
cacheStatic() 是 Vue 编译器中优化性能的关键一步。
它以静态分析为基础,通过递归遍历和分层缓存策略,让模板渲染更高效、更轻量,也体现了 Vue 编译体系的工程化成熟度。
本文部分内容借助 AI 辅助生成,并由作者整理审核。