本文将深入分析 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 辅助生成,并由作者整理审核。