Vue 编译核心:transformMemo 源码深度解析

6 阅读4分钟

本文将深入分析 Vue 编译阶段的一个较为隐蔽但关键的优化钩子——transformMemo
该模块位于 Vue 编译器的 @vue/compiler-core 包中,用于在模板编译阶段处理 v-memo 指令。


一、概念:什么是 v-memo

v-memo 是 Vue 3.2 引入的一种渲染缓存优化指令
其核心作用是:在依赖未变化时,跳过对应子树的渲染与 diff 流程,从而提升性能。

例如:

<div v-memo="[a, b]">{{ a + b }}</div>

当依赖数组 [a, b] 未发生变化时,这个 div 对应的虚拟节点会被直接复用,而不会重新渲染。


二、原理:编译阶段如何插入缓存逻辑?

transformMemo 的核心任务是在编译阶段将带有 v-memo 的节点包裹进 withMemo 调用。

即将:

_createVNode("div", null, _toDisplayString(a + b))

转换为:

_withMemo([a, b], () => _createVNode("div", null, _toDisplayString(a + b)), _cache, 0)

_withMemo 是运行时的缓存辅助函数,负责检测依赖是否变化,并决定是否重渲染。


三、源码分解与注释

以下是完整源码(来自 packages/compiler-core/src/transforms/transformMemo.ts):

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. 引入依赖

import type { NodeTransform } from '../transform'

定义类型:NodeTransform 是编译阶段的节点转换函数类型。

import { findDir } from '../utils'

findDir 用于在节点中查找指定指令(如 v-memo)。

import {
  ElementTypes,
  type MemoExpression,
  NodeTypes,
  type PlainElementNode,
  convertToBlock,
  createCallExpression,
  createFunctionExpression,
} from '../ast'

这些函数与类型定义都属于 AST(抽象语法树)层的辅助工具。
其中:

  • convertToBlock:将普通虚拟节点转化为“Block”节点(能追踪动态节点)。
  • createCallExpression:生成函数调用表达式节点。
  • createFunctionExpression:生成匿名函数表达式节点。

2. 定义弱引用缓存

const seen = new WeakSet()

防止重复处理相同节点。
WeakSet 用于存储已经处理过的节点对象(避免循环依赖或多次访问)。


3. 主转换逻辑

export const transformMemo: NodeTransform = (node, context) => {

transformMemo 是编译阶段的一个 NodeTransform 插件,会在遍历 AST 节点时执行。


4. 过滤条件

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 的返回函数)

return () => {
  const codegenNode =
    node.codegenNode ||
    (context.currentNode as PlainElementNode).codegenNode

Vue 的编译 transform 流程中,返回函数会在 子节点处理完毕后 执行。
此时可以安全地访问生成的 codegenNode


6. 判断与转换

if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {

仅当节点的渲染输出为 VNode 调用时(非文本节点或注释)才进行优化。


7. 转换为 Block 节点

if (node.tagType !== ElementTypes.COMPONENT) {
  convertToBlock(codegenNode, context)
}

v-memo 必须包裹一个可追踪的动态子树,因此非组件节点需要转成 Block 类型。


8. 构造 _withMemo 调用

node.codegenNode = createCallExpression(context.helper(WITH_MEMO), [
  dir.exp!,
  createFunctionExpression(undefined, codegenNode),
  `_cache`,
  String(context.cached.length),
]) as MemoExpression

这一步生成如下伪代码结构:

_withMemo(依赖数组, () => VNode渲染调用, _cache, 缓存索引)

其中:

  • dir.exp!:即模板中 v-memo 的表达式;
  • createFunctionExpression:包装渲染函数;
  • _cache:运行时缓存对象;
  • context.cached.length:缓存编号,用于唯一定位。

9. 递增缓存索引

context.cached.push(null)

每使用一次 v-memo,就增加一次缓存空间。


五、对比:v-oncev-memo

特性v-oncev-memo
缓存机制静态缓存一次动态依赖控制
缓存范围一次性跳过更新条件式跳过更新
使用场景永不变化的节点依赖部分变化的节点
实现机制生成 createStaticVNode包裹 _withMemo 调用

六、实践示例

<template>
  <div v-memo="[count]">
    <p>{{ count }}</p>
  </div>
</template>

在编译结果中:

_withMemo([count], () => (
  _createVNode("div", null, [
    _createVNode("p", null, _toDisplayString(count))
  ])
), _cache, 0)

count 不变时,整个 <div> 节点的渲染函数将被直接复用。


七、拓展:Memo 在 Vue 编译管线中的地位

  • 它属于 指令级 Transform 插件
  • 位于 transformXXX 系列中,与 transformOn, transformBind, transformIf 等平级;
  • 属于性能优化阶段的补充层
  • 与运行时的 withMemo 辅助函数配合使用。

八、潜在问题与限制

  1. 依赖过多时的性能损耗
    v-memo 的依赖会被收集到数组中,每次渲染都需比较,若依赖复杂则会反向影响性能。
  2. 不可嵌套使用
    同一节点多次 v-memo 会被忽略,因为 seen 阻止了重复转换。
  3. 在 SSR 模式中禁用
    context.inSSR 下不会生成 _withMemo,因为服务端渲染本身不需此缓存逻辑。

九、总结

transformMemo 的存在,使得 Vue 能在编译期为开发者自动插入缓存逻辑,从而实现局部的渲染跳过。
它的实现看似简单,却与运行时机制(_withMemo + 缓存数组)形成了紧密的协同,体现了 Vue 编译器“静态化 + 渲染时优化”的一贯设计哲学。


本文部分内容借助 AI 辅助生成,并由作者整理审核。