深入浅出: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 辅助生成,并由作者整理审核。

相关推荐
~无忧花开~21 小时前
JavaScript实现PDF本地预览技巧
开发语言·前端·javascript
小时前端1 天前
“能说说事件循环吗?”—— 我从候选人回答中看到的浏览器与Node.js核心差异
前端·面试·浏览器
IT_陈寒1 天前
Vite 5.0实战:10个你可能不知道的性能优化技巧与插件生态深度解析
前端·人工智能·后端
SAP庖丁解码1 天前
【SAP Web Dispatcher负载均衡】
运维·前端·负载均衡
天蓝色的鱼鱼1 天前
Ant Design 6.0 正式发布:前端开发者的福音与革新
前端·react.js·ant design
HIT_Weston1 天前
38、【Ubuntu】【远程开发】拉出内网 Web 服务:构建静态网页(一)
linux·前端·ubuntu
零一科技1 天前
Vue3拓展:自定义权限指令
前端·vue.js
im_AMBER1 天前
AI井字棋项目开发笔记
前端·笔记·学习·算法
小时前端1 天前
Vuex 响应式原理剖析:构建可靠的前端状态管理
前端·面试·vuex
xiaoxue..1 天前
深入理解浏览器渲染流程:从HTML/CSS/JS到像素呈现
前端·javascript·css·html