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

本文将深入分析 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-oncev-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 辅助函数配合使用。

八、潜在问题与限制

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

九、总结

transformMemo 的存在,使得 Vue 能在编译期为开发者自动插入缓存逻辑,从而实现局部的渲染跳过。

它的实现看似简单,却与运行时机制(_withMemo + 缓存数组)形成了紧密的协同,体现了 Vue 编译器"静态化 + 渲染时优化"的一贯设计哲学。


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

相关推荐
excel5 小时前
Vue 编译器核心 AST 类型系统与节点工厂函数详解
前端
excel5 小时前
Vue 编译器核心:baseCompile 源码深度解析
前端
excel5 小时前
Vue 编译核心:transformModel 深度解析
前端
excel5 小时前
Vue 编译器源码精解:transformOnce 的实现与原理解析
前端
前端架构师-老李5 小时前
React中useContext的基本使用和原理解析
前端·javascript·react.js
Moonbit5 小时前
招募进行时 | MoonBit AI : 程序语言 & 大模型
前端·后端·面试
excel5 小时前
Vue 3 编译器源码深度解析:transformOn —— v-on 指令的编译过程
前端
excel5 小时前
Vue 编译器核心:transformIf 模块深度解析
前端
CodeToGym5 小时前
Vue2 和 Vue3 生命周期的理解与对比
前端·javascript·vue.js