一、概念背景:v-once 的编译机制
在 Vue 的模板语法中,v-once 是一个用于性能优化的指令,作用是:
让绑定的 DOM 节点只渲染一次,并在后续更新中跳过它的 diff 对比。
换句话说,v-once 标记的节点在首次渲染后被视为静态节点,不会因响应式数据变化而重新渲染。
例如:
            
            
              css
              
              
            
          
          <div v-once>{{ msg }}</div>
        在运行时中,即便 msg 发生变化,这个 <div> 也不会被更新。
要实现这一点,Vue 在编译阶段(template → render function)需要特殊处理:
它要在 AST 转换阶段识别 v-once 指令节点,并在代码生成时缓存生成结果,从而跳过后续更新。
这就是 transformOnce 所做的事情。
二、源码原理:AST 转换阶段的工作流程
以下是源码原文(来自 @vue/compiler-core/src/transforms/vOnce.ts):
            
            
              typescript
              
              
            
          
          import type { NodeTransform } from '../transform'
import { findDir } from '../utils'
import { type ElementNode, type ForNode, type IfNode, NodeTypes } from '../ast'
import { SET_BLOCK_TRACKING } from '../runtimeHelpers'
const seen = new WeakSet()
export const transformOnce: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
    if (seen.has(node) || context.inVOnce || context.inSSR) {
      return
    }
    seen.add(node)
    context.inVOnce = true
    context.helper(SET_BLOCK_TRACKING)
    return () => {
      context.inVOnce = false
      const cur = context.currentNode as ElementNode | IfNode | ForNode
      if (cur.codegenNode) {
        cur.codegenNode = context.cache(
          cur.codegenNode,
          true /* isVNode */,
          true /* inVOnce */
        )
      }
    }
  }
}
        三、逐行解析与注释
1. 依赖导入
            
            
              python
              
              
            
          
          import type { NodeTransform } from '../transform'
import { findDir } from '../utils'
import { type ElementNode, type ForNode, type IfNode, NodeTypes } from '../ast'
import { SET_BLOCK_TRACKING } from '../runtimeHelpers'
        NodeTransform:定义了 AST 转换函数的类型签名。findDir:在节点中查找某个指令(例如v-once)。NodeTypes:AST 节点类型枚举,如ELEMENT、TEXT等。SET_BLOCK_TRACKING:运行时 helper,用于控制 block 的依赖追踪。
2. 全局缓存:避免重复处理
            
            
              javascript
              
              
            
          
          const seen = new WeakSet()
        WeakSet用于存储已处理过的节点对象。- 因为 AST 节点是对象引用,
WeakSet能防止重复转换同一节点。 
3. 主函数定义
            
            
              javascript
              
              
            
          
          export const transformOnce: NodeTransform = (node, context) => {
        这里定义了一个符合 NodeTransform 签名的转换函数。
在编译管线中,每个 transform 都会被依次应用到 AST 节点上。
4. 检测是否为带有 v-once 的元素节点
        
            
            
              ini
              
              
            
          
          if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
        - 仅当节点类型为元素(
<div>、<p>等)并且存在v-once指令时才进入。 findDir的第三个参数true表示匹配修饰符版本的指令(如v-oncevsv-once.foo)。
5. 跳过无效场景
            
            
              kotlin
              
              
            
          
          if (seen.has(node) || context.inVOnce || context.inSSR) {
  return
}
        跳过三种情况:
- 节点已经处理过;
 - 当前上下文中已经在 
v-once块内部; - 当前为服务端渲染(
inSSR)模式,v-once无意义。 
6. 注册处理状态与 Helper
            
            
              ini
              
              
            
          
          seen.add(node)
context.inVOnce = true
context.helper(SET_BLOCK_TRACKING)
        - 记录节点已处理。
 - 标记当前上下文处于 
v-once模式。 - 注册运行时 helper 
SET_BLOCK_TRACKING,它在渲染函数中用于启停 block 的依赖跟踪。 
7. 返回后置钩子函数(退出节点时执行)
            
            
              csharp
              
              
            
          
          return () => {
  context.inVOnce = false
  const cur = context.currentNode as ElementNode | IfNode | ForNode
  if (cur.codegenNode) {
    cur.codegenNode = context.cache(
      cur.codegenNode,
      true /* isVNode */,
      true /* inVOnce */
    )
  }
}
        解释如下:
- 
Vue 编译的 transform 通常有进入阶段 和退出阶段 两步。
返回的函数会在退出节点(即子节点处理完成后)调用。
 - 
在这里,它:
- 复位 
context.inVOnce状态; - 获取当前节点的 
codegenNode(即即将生成代码的部分); - 调用 
context.cache将其缓存。 
 - 复位 
 
context.cache 的第二、第三个参数说明:
true /* isVNode */:表示缓存的是 VNode;true /* inVOnce */:标明是v-once节点的缓存。
这样,渲染时就能直接使用缓存结果而不重新创建。
四、与其它 Transform 的对比
| Transform 名称 | 功能 | 与 transformOnce 的区别 | 
|---|---|---|
transformIf | 
处理 v-if / v-else-if / v-else | 
动态条件分支,每次更新需重新计算 | 
transformFor | 
处理 v-for | 
动态列表,需追踪依赖 | 
transformBind | 
处理 v-bind 动态属性 | 
会生成响应式依赖 | 
transformOnce | 
处理 v-once | 
静态缓存,渲染一次后不再更新 | 
简而言之:
transformOnce 是一个静态化 transform ,而其他多数 transform 属于动态依赖 transform。
五、实践:从模板到渲染函数的生成过程
示例模板
            
            
              css
              
              
            
          
          <div v-once>{{ msg }}</div>
        编译后(伪代码)
            
            
              javascript
              
              
            
          
          function render(_ctx, _cache) {
  return _cache[0] || (
    _cache[0] = _createVNode("div", null, _toDisplayString(_ctx.msg), 1)
  )
}
        可以看到:
- 第一次渲染时 
_cache[0]为空,因此执行_createVNode(...); - 结果被缓存;
 - 下次渲染时直接返回缓存,不再重新创建或 diff。
 
这就是 transformOnce 通过 context.cache 实现的逻辑。
六、拓展思考:v-once 的局限与优化策略
- 
v-once只在初次渲染时计算一次;如果内部依赖后续更新,则无法再响应数据变化。
 - 
不适用于需要更新的节点。
 - 
常用于:
- 静态大区块(例如复杂静态表格);
 - 组件初始化时性能优化;
 - SSR 模式中前端 hydration 前的静态标记。
 
 
七、潜在问题与注意点
- 嵌套场景
若v-once嵌套在另一个v-once中,内部 transform 会被跳过(context.inVOnce判断)。 - 服务端渲染(SSR)
SSR 阶段会忽略v-once,因为静态优化对服务器渲染输出无意义。 - 动态组件中使用
若动态组件在v-once内,会导致组件切换不更新,应避免此用法。 
八、总结
transformOnce 是 Vue 编译器中一个非常小但关键的优化模块。
它在 AST 转换阶段识别 v-once,并通过 context.cache 机制让生成的渲染函数在运行时跳过重复 diff,从而提升性能。
本质上,它是"让静态模板节点在编译期缓存"的一种手段,是 Vue 编译器静态优化体系的一部分。
本文部分内容借助 AI 辅助生成,并由作者整理审核。