Appearance
✨ 模板的本质 👌
要点速览
- 模板最终会被编译为“渲染函数”,渲染函数返回虚拟 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 会影响对编译优化与体积的判断。
小结与后续
- 模板是声明式语法糖,最终都会编译为渲染函数,返回 vnode 以驱动更新。
- 运行时不需要模板本身;选择模板或渲染函数应基于团队可读性与场景需求。
- 工程化下的预编译让运行时更轻、更快,可结合
vite-plugin-inspect观察产物。
