深入浅出:Vue 编译器中的 transformText —— 如何把模板文本变成高效的渲染代码

一、概念: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-forv-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 编译器的一个重要理念:

让"模板"尽可能提前变成"精简的渲染逻辑"。


六、潜在问题与注意事项

  1. 带自定义指令的节点不会合并
    因为自定义指令可能修改 DOM,Vue 不敢提前优化。
  2. 空格特殊处理
    如果文本节点内容仅是 ' '(一个空格),Vue 会优化掉,避免多余输出。
  3. SSR 与非 SSR 区别
    SSR 模式下会直接输出字符串,而不是创建 VNode。

七、结语

transformText 虽然只是 Vue 编译流程中一个小环节,但它直接体现了 Vue 3 的**"编译期优化哲学"**:

把模板转成最少、最聪明的渲染指令。

它的目标非常明确------让运行时尽可能"傻瓜化",一切复杂逻辑都留在编译阶段完成。


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

相关推荐
excel5 小时前
Vue 编译器源码深析:transformSlotOutlet 的设计与原理
前端
excel5 小时前
Vue 编译器核心源码解读:transformElement.ts
前端
excel5 小时前
Vue 编译器兼容性系统源码详解
前端
excel5 小时前
Vue 编译器源码解析:noopDirectiveTransform 的作用与设计哲学
前端
uhakadotcom5 小时前
基于 TOON + Next.js 来大幅节省 token 并运行大模型
前端·面试·github
excel5 小时前
🧠 Vue 编译器的表达式处理:transformExpression 通俗讲解
前端
excel5 小时前
一份 TypeScript 声明文件的全景解析:从全局常量到模块扩展
前端
excel5 小时前
Vue 编译器中的过滤器转换机制(transformFilter)详解
前端
excel5 小时前
Vue 编译器中的静态节点缓存机制:cacheStatic() 深度解析
前端