人人都会AI编程

6.2 模板到渲染函数的完整转换过程

更新时间:2026-07-09

Vue 的单文件组件中,<template> 里的 HTML 只是给开发者看的“友好写法”。浏览器不认识它,Vue 的运行时也不直接用它来渲染。真正起作用的是渲染函数,而模板到渲染函数的转换,由 Vue 编译器在构建阶段(使用 Vite/Webpack 时)或运行时(使用 CDN 引入完整版时)自动完成。

这个过程可以看作一条流水线:模板 → AST → 优化 → 可执行代码。下面用一段简单的模板带你看完整个流程。

假设我们在组件里写了这样一个模板:

<template>
  <div id="app">
    <p>Hello, {{ name }}!</p>
    <span>静态文本,永不改变</span>
  </div>
</template>

阶段一:解析(Parse)—— 把 HTML 字符串变成 AST

编译器首先用状态机扫描模板字符串,识别出开始/结束标签、属性、文本、插值表达式等元素,并构建出一棵抽象语法树(AST)。每个节点都记录了标签名、属性、指令、子节点等信息,就像浏览器解析 HTML 生成 DOM 树一样,但这里的“节点”是纯粹的 JavaScript 对象,描述结构而非真实渲染。

对于上面的模板,AST 的结构可以近似理解为:

{
  type: 1, // 元素节点
  tag: 'div',
  props: [{ name: 'id', value: 'app' }],
  children: [
    {
      type: 1,
      tag: 'p',
      children: [
        { type: 2, text: 'Hello, ' },          // 静态文本片段
        { type: 5, content: { type: 4, content: 'name' } }, // 插值节点
        { type: 2, text: '!' }
      ]
    },
    {
      type: 1,
      tag: 'span',
      children: [{ type: 2, text: '静态文本,永不改变' }]
    }
  ]
}

AST 里的 type 编码:1=元素,2=文本,5=插值,4=简单表达式。真实编译器的 AST 会包含更多细节(如指令绑定、事件处理),但核心逻辑一样。

阶段二:优化(Optimize)—— 标记静态节点与动态节点

有了 AST 之后,Vue 3 的编译器会遍历整棵树,给每个节点打上“静态”或“动态”的标记。

  • 静态节点:节点本身及其所有子节点都不包含任何响应式数据绑定(如 {{ }}:attrv- 指令),完全在编译期就能确定结构。例如这里的 <span>静态文本,永不改变</span>
  • 动态节点:依赖了响应式数据,可能会在未来变化。例如 <p>Hello, {{ name }}!</p> 里的 {{ name }} 就是动态部分。

这个阶段的核心任务是识别出静态根节点动态绑定,为后续的运行时 Diff 提供关键信息。对于动态节点,编译器还会生成一个PatchFlag标记,精确指出该节点哪些部分会变。比如,这里的 <p> 标签内部有文本插值,PatchFlag 会被标记为 1 / TEXT /,表示只有文本内容需要对比更新,而 tagprops 不会变。

优化阶段还会把不会变的静态节点提升(hoist)到渲染函数作用域外部。这意味着这些节点对应的 VNode 在组件多次渲染时只创建一次并复用,完全跳过 Diff 过程。在我们的例子里,<span> 部分会被提升为常量。

阶段三:生成(Generate)—— 把 AST 转换为可执行的渲染函数代码

最后一步,编译器根据优化后的 AST,生成一个可执行的 JavaScript 函数代码字符串(通常由构建工具将其编译为真正的 JS 函数)。

生成的代码大致长这样:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "静态文本,永不改变", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", { id: "app" }, [
    _createElementVNode("p", null, "Hello, " + _toDisplayString(_ctx.name) + "!", 1 /* TEXT */),
    _hoisted_1
  ]))
}

这段代码可能看起来有些复杂,但核心思路很清晰:

  • 静态节点被提取成 _hoisted_1 常量,在组件初始化时一次性创建 VNode,后续更新直接复用,不参与 Diff。
  • 动态文本 {{ name }} 变成了 _toDisplayString(_ctx.name),每次渲染时都会读取最新的响应式值并生成文本,同时 _createElementVNode 的最后一个参数 1 / TEXT / 就是 PatchFlag,告诉 Diff 算法只检查这个节点的文本内容。
  • 最外层_openBlock()_createElementBlock 创建了一个“块”(Block),它内部记录所有动态子节点。这样,在 Diff 的时候,Vue 只需要遍历这个 Block 中动态子节点的集合,而完全跳过那些静态的、已提升的节点,极大提升了更新效率。

在运行时,当组件的响应式状态发生变化触发重新渲染时,Vue 不会重新解析模板,而是直接执行这个已经生成好的渲染函数,得到一棵新的 VNode 树。然后通过 Diff 算法对比新旧 VNode,只更新那些真正变化的 DOM 部分。

开发者的实际感知

你不需要手动编写这些代码,也不需要关心 PatchFlag 的数值含义。构建工具(Vite 的 @vitejs/plugin-vue)会自动在打包时完成编译,你只需写好 .vue 文件里的 <template>。这种编译手段让 Vue 同时获得了开发时的直观性运行时的性能:开发者享受 HTML 的可读性,用户得到的是经过深度优化的渲染逻辑。

如果你想看看自己的模板最终被编译成什么样子,可以在 Vue 3 Template Explorer 中粘贴模板,它会实时展现编译结果,并标注出静态提升、PatchFlags 等优化细节。这也是理解和调试模板编译的最佳实践工具。