【重学 Vue:深入本质,脱离八股文,彻底搞定面试】:(二)模板的本质

212 阅读4分钟

Vue 的模板语法是我们日常开发中最常使用的部分,但它背后的核心其实是「渲染函数」与「模板编译器」。本文将从本质出发,深入探讨模板是如何被编译成渲染函数的,以及编译发生的时机。


🧩 本文结构:

  1. 渲染函数
  2. 模板编译原理
  3. 编译的时机

一、渲染函数

📚 概念:

文档地址:渲染函数官方文档

渲染函数(h 函数)调用后返回的是 虚拟 DOM 节点

💡 在 Vue 中,我们所写的模板会被一个 模板编译器 编译成 JS 渲染函数。因此,Vue 本质上并不依赖模板,而是依赖渲染函数。

这意味着你可以直接使用纯 JavaScript 描述组件视图结构,下面是一个经典的示例:

// 模板组件

<template>
  <div class="user-card">
    <img :src="user.avatarUrl" alt="User avatar" class="avatar" />
    <div class="user-info">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
  </div>
</template>
<script setup>
const user = defineProps({
  name: String,
  email: String,
  avatarUrl: String
})
</script>

// 使用渲染函数重写模板组件

import { defineComponent, h } from 'vue'
import styles from './UserCard.module.css'

export default defineComponent({
  name: 'UserCard',
  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)
        ])
      ])
  }
})

对应的样式:

/* 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;
}

都得到相同的效果图如下:

✅ 总结:

Vue 提供模板只是为了降低开发者的心智负担,而 Vue 核心只关心渲染函数所返回的虚拟 DOM。

Vue 模板如何转换为 JS 渲染函数呢?这就要说说模板编译原理了...


二、模板编译原理

📌模板本质上是一段字符串,模板编译器需要对这串字符串进行操作,最终生成渲染函数。

编译过程详解:

模板编译器结构概览:

function compile(template){
  const ast = parse(template)         // 1. 解析器
  transform(ast)                      // 2. 转换器
  const code = generate(ast)          // 3. 生成器
  return code
}

示例:简单模板的编译过程

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

模板编译器将其看成一串字符串:

"<div><p>Vue</p><p>React</p></div>"

Step 1:解析器将字符串解析生成 tokens

[
  { 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' }
]

Step 2:解析器根据 tokens 生成模板 AST(抽象语法树)

{
  type: 'Root',
  children: [
    {
      type: 'Element',
      tag: 'div',
      children: [
        {
          type: 'Element',
          tag: 'p',
          children: [{ type: 'Text', content: 'Vue' }]
        },
        {
          type: 'Element',
          tag: 'p',
          children: [{ type: 'Text', content: 'React' }]
        }
      ]
    }
  ]
}

Step 3:转换器将模板 AST 转换为 JS AST

{
  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' }
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

Step 4:生成器根据 JS AST 生成最终的 JS 代码

function render () {
  return h('div', [h('p', 'Vue'), h('p', 'React')])
}

✅ 总结:

模板本质上是一段字符串,模板编译器分为三步对这串字符串进行操作,最终生成渲染函数:

  1. 解析器(Parser):将字符串解析生成 tokens , 根据 tokens 生成模板 AST(抽象语法树)
  2. 转换器(Transformer):将模板 AST 转换为 JS AST
  3. 生成器(Code Generator):将 JS AST 生成最终的渲染函数代码

每一个部件都依赖于上一个部件的执行结果。


三、编译的时机

1. 运行时编译

当我们使用 CDN 引入 Vue,并在 HTML 中直接写模板时,Vue 会在运行时进行模板编译:

<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" 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 = {
    props: ['name', 'email', 'avatarUrl'],
    template: '#user-card-template'
  }

  createApp({
    components: { UserCard },
    data() {
      return {
        name: "夕夕",
        email: "123@qq.com",
        avatarUrl: './avater.jpg',
      }
    }
  }).mount('#app')
</script>

💡 这种方式的性能较差,适合快速原型开发或教学演示。

2. 预编译(推荐)

在 Vite/Webpack 等构建工具中,Vue 会在打包阶段就把模板预编译成渲染函数,浏览器拿到的是已经编译好的代码。

推荐使用 vite-plugin-inspect 插件查看编译结果:

npm install vite-plugin-inspect --save-dev

vite.config.js 中配置:

import Inspect from 'vite-plugin-inspect'

export default {
  plugins: [
    Inspect()
  ]
}

运行后可访问:http://localhost:5173/__inspect/ 查看每个组件编译后的渲染函数。

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _createElementVNode("img", {
      src: $setup.user.avatarUrl,
      alt: "User avatar",
      class: "avatar",
      "data-v-inspector": "src/App.vue:3:5"
    }, null, 8 /* PROPS */, _hoisted_2),
    _createElementVNode("div", _hoisted_3, [
      _createElementVNode("h2", _hoisted_4, _toDisplayString($setup.user.name), 1 /* TEXT */),
      _createElementVNode("p", _hoisted_5, _toDisplayString($setup.user.email), 1 /* TEXT */)
    ])
  ]))
}

🧠 总结

  • Vue 模板只是语法糖,本质是渲染函数。
  • 模板编译分为解析器 → 转换器 → 生成器三大步骤。
  • Vue 支持运行时编译(不推荐)与预编译(推荐)。
  • 学会使用渲染函数有助于更深入理解 Vue 的本质。

📌 下一篇:【重学 Vue:深入本质,脱离八股文,彻底搞定面试】:(三)组件树和虚拟DOM树

敬请期待~


如果你觉得这篇文章对你有帮助,欢迎关注 + 点赞 + 收藏,我会持续输出「Vue 技术本质」系列内容。