Skip to content

✨ 模板的本质 👌

要点速览

  • 模板最终会被编译为“渲染函数”,渲染函数返回虚拟 DOM(VNode)。
  • Vue 运行时不需要模板本身;只需要渲染函数来描述 UI 结构。
  • 同一视图可用“模板”或“纯 JS 渲染函数(h)/JSX”来书写。
  • 编译管线:解析(Parse)→ 转换(Transform)→ 生成(Codegen)。
  • 编译时机分为运行时编译(CDN 场景)与预编译(工程化打包)。

快速上手

下面用一个最小示例直观理解“同一视图结构”,分别用模板与渲染函数来描述:

vue
<!-- 模板方式:UserCard.vue -->
<template>
  <div class="user-card">
    <img :src="avatarUrl" alt="User avatar" class="avatar" />
    <div class="user-info">
      <h2>{{ name }}</h2>
      <p>{{ email }}</p>
    </div>
  </div>
</template>

<script setup>
defineProps({ name: String, email: String, avatarUrl: String });
</script>

<style scoped>
.user-card {
  display: flex;
  align-items: center;
  background-color: #f9f9f9;
  border: 1px solid #e0e0e0;
  border-radius: 10px;
  padding: 10px;
  margin: 10px 0;
}
.avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  margin-right: 15px;
}
.user-info h2 {
  margin: 0;
  font-size: 20px;
  color: #333;
}
.user-info p {
  margin: 5px 0 0;
  font-size: 16px;
  color: #666;
}
</style>
js
// 纯 JS 渲染函数:等价视图(h 返回 vnode)
import { defineComponent, h } from "vue";
import styles from "./UserCard.module.css";

export default defineComponent({
  name: "UserCardRender",
  props: { name: String, email: String, avatarUrl: String },
  setup(props) {
    return () =>
      h("div", { class: styles.userCard }, [
        h("img", {
          class: styles.avatar,
          src: props.avatarUrl,
          alt: "User avatar",
        }),
        h("div", { class: styles.userInfo }, [
          h("h2", props.name),
          h("p", props.email),
        ]),
      ]);
  },
});
css
/* UserCard.module.css */
.userCard {
  display: flex;
  align-items: center;
  background-color: #f9f9f9;
  border: 1px solid #e0e0e0;
  border-radius: 10px;
  padding: 10px;
  margin: 10px 0;
}
.avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  margin-right: 15px;
}
.userInfo h2 {
  margin: 0;
  font-size: 20px;
  color: #333;
}
.userInfo p {
  margin: 5px 0 0;
  font-size: 16px;
  color: #666;
}

关键认识

  • 渲染函数 h() 返回的是虚拟 DOM(普通 JS 对象),不是直接创建真实 DOM。
  • 模板只是更易读的“声明式语法糖”;最终都会变成渲染函数供运行时调用。

核心概念

渲染函数与 VNode

  • 渲染函数是“用 JS 描述 UI 的函数”,返回 vnode(虚拟 DOM)。
  • vnode 是普通对象(type/props/children 等),框架据此计算更新并打补丁到真实 DOM。
js
import { h } from "vue";
const vnode = h("div", { class: "box" }, [h("span", null, "Hello")]);
console.log(vnode);
// { type: 'div', props: { class: 'box' }, children: [ { type: 'span', children: 'Hello' } ] }

模板如何被编译

单文件组件中的模板,本质是字符串,编译器会把它编译为渲染函数。

模板:

html
<template>
  <div>
    <h1 :id="someId">Hello</h1>
  </div>
</template>

编译器视角:只是字符串,需要编译为可执行的渲染函数代码:

js
function render() {
  return h("div", [h("h1", { id: someId }, "Hello")]);
}

编译管线(简版)

  • 解析(Parser):将模板字符串解析为模板 AST(抽象语法树)。
  • 转换(Transform):将模板 AST 转换为 JS AST。
  • 生成(Codegen):将 JS AST 生成最终的渲染函数代码。

示例:

html
<div>
  <p>Vue</p>
  <p>React</p>
</div>

解析得到的 tokens(示意):

js
[
  { type: "tag", name: "div" },
  { type: "tag", name: "p" },
  { type: "text", content: "Vue" },
  { type: "tagEnd", name: "p" },
  { type: "tag", name: "p" },
  { type: "text", content: "React" },
  { type: "tagEnd", name: "p" },
  { type: "tagEnd", name: "div" },
];

转换得到的 JS AST(示意):

js
{
  type: "FunctionDecl",
  id: { type: "Identifier", name: "render" },
  params: [],
  body: [
    {
      type: "ReturnStatement",
      return: {
        type: "CallExpression",
        callee: { type: "Identifier", name: "h" },
        arguments: [
          { type: "StringLiteral", value: "div" },
          {
            type: "ArrayExpression",
            elements: [
              { type: "CallExpression", callee: { type: "Identifier", name: "h" }, arguments: [{ type: "StringLiteral", value: "p" }, { type: "StringLiteral", value: "Vue" }] },
              { type: "CallExpression", callee: { type: "Identifier", name: "h" }, arguments: [{ type: "StringLiteral", value: "p" }, { type: "StringLiteral", value: "React" }] },
            ],
          },
        ],
      },
    },
  ],
}

生成最终渲染函数:

js
function render() {
  return h("div", [h("p", "Vue"), h("p", "React")]);
}

编译器整体结构(示例伪码):

js
function compile(template) {
  const ast = parse(template); // 1. 解析器
  transform(ast); // 2. 转换器:模板 AST → JS AST
  const code = generate(ast); // 3. 生成器
  return code;
}

模板编译的时机

整体分两类场景:

运行时编译(CDN 场景)

  • 直接通过 CDN 引入 Vue,组件的 template 会在运行时被编译。
  • 适用于快速原型或在线示例,省去构建步骤,但运行时体积更大、性能略有开销。

示例:

html
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Runtime Compile</title>
  </head>
  <body>
    <div id="app">
      <user-card :name="name" :email="email" :avatar-url="avatarUrl" />
    </div>

    <template id="user-card-template">
      <div class="user-card">
        <img :src="avatarUrl" alt="User avatar" class="avatar" />
        <div class="user-info">
          <h2>{{ name }}</h2>
          <p>{{ email }}</p>
        </div>
      </div>
    </template>

    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script>
      const { createApp } = Vue;
      const UserCard = {
        name: "UserCard",
        props: { name: String, email: String, avatarUrl: String },
        template: "#user-card-template",
      };

      createApp({
        components: { UserCard },
        data: () => ({
          name: "John",
          email: "john@example",
          avatarUrl: "./yinshi.jpg",
        }),
      }).mount("#app");
    </script>
  </body>
</html>

预编译(工程化场景)

  • 在构建阶段(如 Vite),模板会被编译为渲染函数,浏览器拿到的产物中不再包含模板。
  • 好处:更小的运行时、更快的加载、更好的性能与可优化空间。

辅助工具(查看编译结果):

js
// vite.config.ts / vite.config.js
import Inspect from "vite-plugin-inspect";
export default { plugins: [Inspect()] };
// 运行后访问 http://localhost:5173/__inspect/ 查看每个组件的编译产物

模板 vs 渲染函数:何时选择?

选择建议

  • 日常业务组件:模板可读性更好、团队心智负担更低,优先使用模板。
  • 高度动态的结构拼装、可视化库/渲染器、指令式创建复杂树时:考虑 h()/JSX。
  • 需要精细控制 vnode、动态 slot/props 合成、按条件批量生成节点:h()/JSX 更灵活。

对比示例(同一 UI,模板更易读,渲染函数更灵活):

vue
<template>
  <ul>
    <li v-for="item in items" :key="item.id">{{ item.label }}</li>
  </ul>
</template>

<script setup>
defineProps({ items: Array });
</script>
js
import { h } from "vue";
export default {
  props: { items: Array },
  render() {
    return h(
      "ul",
      this.items.map((item) => h("li", { key: item.id }, item.label))
    );
  },
};

常见误区

易错点

  • 误以为“浏览器保留模板并直接执行”:实际上运行时执行的是渲染函数。
  • 误把 h() 当作“创建真实 DOM”:它返回的是 vnode,由框架负责落地到真实 DOM。
  • 认为“渲染函数一定更快”:性能差异取决于场景与编译优化,模板经预编译通常表现很好。
  • 忽视编译管线:不了解 Parse/Transform/Codegen 会影响对编译优化与体积的判断。

小结与后续

  1. 模板是声明式语法糖,最终都会编译为渲染函数,返回 vnode 以驱动更新。
  2. 运行时不需要模板本身;选择模板或渲染函数应基于团队可读性与场景需求。
  3. 工程化下的预编译让运行时更轻、更快,可结合 vite-plugin-inspect 观察产物。