本文将深入分析 Vue 编译阶段的一个较为隐蔽但关键的优化钩子------transformMemo。
该模块位于 Vue 编译器的 @vue/compiler-core 包中,用于在模板编译阶段处理 v-memo 指令。
一、概念:什么是 v-memo?
v-memo 是 Vue 3.2 引入的一种渲染缓存优化指令 。
其核心作用是:在依赖未变化时,跳过对应子树的渲染与 diff 流程,从而提升性能。
例如:
css
<div v-memo="[a, b]">{{ a + b }}</div>
当依赖数组 [a, b] 未发生变化时,这个 div 对应的虚拟节点会被直接复用,而不会重新渲染。
二、原理:编译阶段如何插入缓存逻辑?
transformMemo 的核心任务是在编译阶段将带有 v-memo 的节点包裹进 withMemo 调用。
即将:
css
_createVNode("div", null, _toDisplayString(a + b))
转换为:
css
_withMemo([a, b], () => _createVNode("div", null, _toDisplayString(a + b)), _cache, 0)
_withMemo 是运行时的缓存辅助函数,负责检测依赖是否变化,并决定是否重渲染。
三、源码分解与注释
以下是完整源码(来自 packages/compiler-core/src/transforms/transformMemo.ts):
typescript
import type { NodeTransform } from '../transform'
import { findDir } from '../utils'
import {
ElementTypes,
type MemoExpression,
NodeTypes,
type PlainElementNode,
convertToBlock,
createCallExpression,
createFunctionExpression,
} from '../ast'
import { WITH_MEMO } from '../runtimeHelpers'
const seen = new WeakSet()
export const transformMemo: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const dir = findDir(node, 'memo')
if (!dir || seen.has(node) || context.inSSR) {
return
}
seen.add(node)
return () => {
const codegenNode =
node.codegenNode ||
(context.currentNode as PlainElementNode).codegenNode
if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
// 非组件元素转为 Block,以启用动态节点追踪
if (node.tagType !== ElementTypes.COMPONENT) {
convertToBlock(codegenNode, context)
}
// 用 _withMemo 包裹渲染表达式
node.codegenNode = createCallExpression(context.helper(WITH_MEMO), [
dir.exp!, // 缓存依赖数组表达式
createFunctionExpression(undefined, codegenNode), // 渲染函数
`_cache`, // 缓存对象
String(context.cached.length), // 缓存索引
]) as MemoExpression
context.cached.push(null) // 增加缓存计数
}
}
}
}
四、逐行解析(详细注释)
1. 引入依赖
python
import type { NodeTransform } from '../transform'
定义类型:NodeTransform 是编译阶段的节点转换函数类型。
javascript
import { findDir } from '../utils'
findDir 用于在节点中查找指定指令(如 v-memo)。
python
import {
ElementTypes,
type MemoExpression,
NodeTypes,
type PlainElementNode,
convertToBlock,
createCallExpression,
createFunctionExpression,
} from '../ast'
这些函数与类型定义都属于 AST(抽象语法树)层的辅助工具。
其中:
convertToBlock:将普通虚拟节点转化为"Block"节点(能追踪动态节点)。createCallExpression:生成函数调用表达式节点。createFunctionExpression:生成匿名函数表达式节点。
2. 定义弱引用缓存
javascript
const seen = new WeakSet()
防止重复处理相同节点。
WeakSet 用于存储已经处理过的节点对象(避免循环依赖或多次访问)。
3. 主转换逻辑
javascript
export const transformMemo: NodeTransform = (node, context) => {
transformMemo 是编译阶段的一个 NodeTransform 插件,会在遍历 AST 节点时执行。
4. 过滤条件
ini
if (node.type === NodeTypes.ELEMENT) {
const dir = findDir(node, 'memo')
if (!dir || seen.has(node) || context.inSSR) {
return
}
}
- 仅对 元素节点 进行处理;
- 若未找到
v-memo指令,则直接返回; - 若在 SSR 模式下(
context.inSSR),则禁用; - 若节点已处理过,则跳过。
5. 延迟回调(transform 的返回函数)
javascript
return () => {
const codegenNode =
node.codegenNode ||
(context.currentNode as PlainElementNode).codegenNode
Vue 的编译 transform 流程中,返回函数会在 子节点处理完毕后 执行。
此时可以安全地访问生成的 codegenNode。
6. 判断与转换
ini
if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
仅当节点的渲染输出为 VNode 调用时(非文本节点或注释)才进行优化。
7. 转换为 Block 节点
scss
if (node.tagType !== ElementTypes.COMPONENT) {
convertToBlock(codegenNode, context)
}
v-memo 必须包裹一个可追踪的动态子树,因此非组件节点需要转成 Block 类型。
8. 构造 _withMemo 调用
javascript
node.codegenNode = createCallExpression(context.helper(WITH_MEMO), [
dir.exp!,
createFunctionExpression(undefined, codegenNode),
`_cache`,
String(context.cached.length),
]) as MemoExpression
这一步生成如下伪代码结构:
scss
_withMemo(依赖数组, () => VNode渲染调用, _cache, 缓存索引)
其中:
dir.exp!:即模板中v-memo的表达式;createFunctionExpression:包装渲染函数;_cache:运行时缓存对象;context.cached.length:缓存编号,用于唯一定位。
9. 递增缓存索引
csharp
context.cached.push(null)
每使用一次 v-memo,就增加一次缓存空间。
五、对比:v-once 与 v-memo
| 特性 | v-once |
v-memo |
|---|---|---|
| 缓存机制 | 静态缓存一次 | 动态依赖控制 |
| 缓存范围 | 一次性跳过更新 | 条件式跳过更新 |
| 使用场景 | 永不变化的节点 | 依赖部分变化的节点 |
| 实现机制 | 生成 createStaticVNode |
包裹 _withMemo 调用 |
六、实践示例
xml
<template>
<div v-memo="[count]">
<p>{{ count }}</p>
</div>
</template>
在编译结果中:
javascript
_withMemo([count], () => (
_createVNode("div", null, [
_createVNode("p", null, _toDisplayString(count))
])
), _cache, 0)
当 count 不变时,整个 <div> 节点的渲染函数将被直接复用。
七、拓展:Memo 在 Vue 编译管线中的地位
- 它属于 指令级 Transform 插件;
- 位于
transformXXX系列中,与transformOn,transformBind,transformIf等平级; - 属于性能优化阶段的补充层;
- 与运行时的
withMemo辅助函数配合使用。
八、潜在问题与限制
- 依赖过多时的性能损耗 :
v-memo的依赖会被收集到数组中,每次渲染都需比较,若依赖复杂则会反向影响性能。 - 不可嵌套使用 :
同一节点多次v-memo会被忽略,因为seen阻止了重复转换。 - 在 SSR 模式中禁用 :
context.inSSR下不会生成_withMemo,因为服务端渲染本身不需此缓存逻辑。
九、总结
transformMemo 的存在,使得 Vue 能在编译期为开发者自动插入缓存逻辑,从而实现局部的渲染跳过。
它的实现看似简单,却与运行时机制(_withMemo + 缓存数组)形成了紧密的协同,体现了 Vue 编译器"静态化 + 渲染时优化"的一贯设计哲学。
本文部分内容借助 AI 辅助生成,并由作者整理审核。