一、概念:transformText 是干什么的?
在 Vue 模板中,我们经常写这样的代码:
            
            
              css
              
              
            
          
          <div>Hello {{ name }} !</div>
        从模板到运行时渲染,中间要经过"编译"。
Vue 会先把模板解析成一棵 AST(抽象语法树) ,然后对它做一系列"转换(transform)",最后再生成可执行的渲染函数。
其中,transformText 就是这些转换之一,它专门负责:
把模板中的「相邻文本和插值表达式」合并成一个高效的表达式。
比如原始模板有三个节点:
            
            
              vbnet
              
              
            
          
          [ Text("Hello "), Interpolation(name), Text("!") ]
        transformText 会把它变成一个表达式:
            
            
              arduino
              
              
            
          
          "Hello " + name + "!"
        并生成调用:
            
            
              arduino
              
              
            
          
          createTextVNode("Hello " + name + "!", 1 /* TEXT */)
        这样在运行时就只需要创建一个文本节点,而不是三个,从而提升性能。
二、原理:一步步看懂 transformText
源码位于 Vue 编译器的 transform 阶段。我们来拆解它。
1️⃣ 入口与条件判断
            
            
              ini
              
              
            
          
          export const transformText: NodeTransform = (node, context) => {
  if (
    node.type === NodeTypes.ROOT ||
    node.type === NodeTypes.ELEMENT ||
    node.type === NodeTypes.FOR ||
    node.type === NodeTypes.IF_BRANCH
  ) {
    return () => { ... }
  }
}
        解释:
- 
NodeTransform是一个"节点转换函数"。 - 
Vue 在遍历 AST 时,会给每个节点调用相应的 transform。
 - 
这里只对「有子节点的结构」才处理,比如:
- 根节点(
ROOT) - 元素(
ELEMENT) v-for或v-if分支
 - 根节点(
 
📌 关键点 :transformText 只处理"容器型节点"的子节点。
2️⃣ 延迟执行:为什么要 "return 一个函数"
Vue 的编译分为两步:进入节点 → 退出节点 。
return () => {} 表示这段逻辑会在节点"退出"时执行(也就是它的所有子节点都处理完之后)。
因为我们只有等插值表达式({{ name }})已经被其他 transform 处理好,才能正确地合并文本。
3️⃣ 合并相邻的文本节点
            
            
              ini
              
              
            
          
          for (let i = 0; i < children.length; i++) {
  const child = children[i]
  if (isText(child)) {
    hasText = true
    for (let j = i + 1; j < children.length; j++) {
      const next = children[j]
      if (isText(next)) {
        if (!currentContainer) {
          currentContainer = children[i] = createCompoundExpression([child], child.loc)
        }
        currentContainer.children.push(` + `, next)
        children.splice(j, 1)
        j--
      } else {
        currentContainer = undefined
        break
      }
    }
  }
}
        逐行讲解:
isText(child):判断是不是文本或插值节点。- 如果下一个也是文本,就说明它们是相邻的。
 - 创建一个复合表达式(
createCompoundExpression),把两个文本拼起来。 - 用字符串 
" + "连接,模拟字符串拼接效果。 - 把多余的节点删掉(
children.splice(j, 1))。 
📘 举例:
            
            
              css
              
              
            
          
          <div>Hi {{ user }} !</div>
        会变成:
            
            
              less
              
              
            
          
          createCompoundExpression([
  Text("Hi "), " + ", Interpolation(user), " + ", Text("!")
])
        4️⃣ 跳过无需处理的情况
            
            
              matlab
              
              
            
          
          if (!hasText ||
  (children.length === 1 && node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT)
) {
  return
}
        意思是:
- 如果没发现文本节点,直接跳过;
 - 如果只有一个纯文本节点,比如 
<div>hello</div>,也不用转成createTextVNode,因为运行时可以直接用el.textContent = "hello",更快。 
Vue 会自动区分这些情况,避免多余代码。
5️⃣ 把合并后的文本节点包装成 createTextVNode
        
            
            
              css
              
              
            
          
          for (let i = 0; i < children.length; i++) {
  const child = children[i]
  if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) {
    const callArgs: CallExpression['arguments'] = []
    if (child.type !== NodeTypes.TEXT || child.content !== ' ') {
      callArgs.push(child)
    }
    if (!context.ssr && getConstantType(child, context) === ConstantTypes.NOT_CONSTANT) {
      callArgs.push(PatchFlags.TEXT)
    }
    children[i] = {
      type: NodeTypes.TEXT_CALL,
      codegenNode: createCallExpression(
        context.helper(CREATE_TEXT),
        callArgs
      ),
    }
  }
}
        作用:
- 把合并结果包装成调用:
createTextVNode("Hello " + name + "!", PatchFlags.TEXT) - 第二个参数 
PatchFlags.TEXT表示"这个文本在更新时可能会变化"。
Vue runtime 会用它来决定是否重新渲染该节点。 
三、对比分析:Vue 2 vs Vue 3
| 对比项 | Vue 2.x | Vue 3.x (transformText) | 
|---|---|---|
| 文本合并 | 在运行时拼接字符串 | 在编译阶段提前合并 | 
| 性能 | 多节点 patch,慢 | 单节点 patch,快 | 
| 可维护性 | 运行时逻辑复杂 | 编译阶段处理更清晰 | 
| 动态标识 | 无 | 使用 PatchFlags.TEXT 精确标记 | 
Vue 3 的编译优化思想就是------ "把能在编译时做的事都提前做" 。
四、实践:看一个完整的编译结果
模板:
            
            
              css
              
              
            
          
          <div>Hello {{ user }} !</div>
        转换后 AST 的关键部分:
            
            
              css
              
              
            
          
          {
  type: TEXT_CALL,
  codegenNode: createCallExpression(
    CREATE_TEXT,
    [
      createCompoundExpression([
        Text("Hello "),
        " + ",
        Interpolation(user),
        " + ",
        Text("!")
      ]),
      PatchFlags.TEXT
    ]
  )
}
        最终生成渲染代码:
            
            
              kotlin
              
              
            
          
          return _createElementVNode("div", null, [
  _createTextVNode("Hello " + _toDisplayString(user) + "!", 1 /* TEXT */)
])
        这样,Vue 在运行时只需更新 user 变化的地方,而不是重新 diff 整个结构。
五、拓展思考:为什么 Vue 要这么做?
- 性能优化:减少 VNode 数量和创建次数。
 - 内存优化:一个文本节点取代多个,节省内存。
 - 运行时开销减少:静态内容不再需要比较。
 - SSR 一致性:在服务端也能生成相同结构的字符串。
 
这其实体现了 Vue 3 编译器的一个重要理念:
让"模板"尽可能提前变成"精简的渲染逻辑"。
六、潜在问题与注意事项
- 带自定义指令的节点不会合并
因为自定义指令可能修改 DOM,Vue 不敢提前优化。 - 空格特殊处理
如果文本节点内容仅是' '(一个空格),Vue 会优化掉,避免多余输出。 - SSR 与非 SSR 区别
SSR 模式下会直接输出字符串,而不是创建 VNode。 
七、结语
transformText 虽然只是 Vue 编译流程中一个小环节,但它直接体现了 Vue 3 的**"编译期优化哲学"**:
把模板转成最少、最聪明的渲染指令。
它的目标非常明确------让运行时尽可能"傻瓜化",一切复杂逻辑都留在编译阶段完成。
本文部分内容借助 AI 辅助生成,并由作者整理审核。