阿卡不拉阿卡不拉
Vue3
阿卡的博客
Vue3
阿卡的博客
  • Vue3

    • 快速入门

      • 搭建工程 👌
      • 模板语法
      • 响应式基础
      • 响应式常用 API
      • 计算属性
      • 类与样式绑定
      • 条件和列表渲染
      • 事件处理
      • 表单处理
      • 生命周期
      • 侦听器
      • 组件介绍
      • Props
      • 自定义事件
      • 组件v-model
      • 插槽
      • 前端路由介绍
      • KeepAlive内置组件
      • 状态管理库
      • 组件库介绍
    • 深入本质

      • 虚拟DOM本质
      • 模板的本质
      • 组件树和虚拟DOM树
      • 数据拦截的本质
      • 响应式数据的本质
      • 响应式的本质
      • 响应式和组件渲染
      • 实现响应式系统 1
      • 实现响应式系统 2
      • 图解EFFECT
      • 实现响应式系统 3
      • 手写computed
      • 手写watch
      • 指令的本质
      • 插槽的本质
      • v-model的本质
      • setup 语法标签
      • 组件生命周期
      • keepalive 生命周期
      • keepalive的本质
      • key的本质
    • 细节补充

      • 【Vue】属性透传
      • 【Vue】依赖注入
      • 【Vue】组合式函数 👌
      • 【Vue】自定义指令
      • 【Vue】插件
      • 【Vue】Transition
      • 【Vue】TransitionGroup
      • 【Vue】Teleport
      • 【Vue】异步组件
      • 【Vue】Suspense
      • 【Router】路由模式
      • 【Router】路由零碎知识
      • 【Router】路由匹配语法
      • 【Router】路由组件传参
      • 【Router】内置组件和函数
      • 【Router】导航守卫
      • 【Router】过渡特效
      • 【Router】滚动行为
      • 【Router】动态路由
      • 【State】通信方式总结
      • 【State】Pinia 自定义插件
      • 【场景】封装树形组件
      • 【场景】自定义 ref 实现防抖
      • 【场景】懒加载
      • 【场景】虚拟列表
      • 【场景】虚拟列表优化
      • 【第三方库】VueUse
      • 【第三方库】vuedragable
      • 【第三方库】vue-drag-resize
      • 【第三方库】vue-chartjs
      • 【第三方库】vuelidate
      • 【第三方库】vue3-lazyload
      • 【场景】Websocket 聊天室
      • 【Vite】✨ 认识 Vite👌
      • 【Vite】配置文件 👌
      • 【Vite】✨ 依赖预构建 👌
      • 【Vite】构建生产版本 👌
      • 【Vite】环境变量与模式
      • 【Vite】CLI
      • 【Vite】Vite 插件
  • 笔面试

    • HTML

      • HTML 面试题汇总
      • 文档声明
      • 语义化
      • W3C 标准组织
      • SEO
      • iframe
      • 微格式
      • 替换元素
      • 页面可见性
    • CSS

      • CSS 面试题汇总
      • CSS 单位总结
      • 居中方式总结
      • 隐藏元素方式总结
      • 浮动
      • 定位总结
      • BFC
      • CSS 属性计算过程
      • CSS 层叠继承规则总结
      • @import 指令
      • CSS3 calc 函数
      • CSS3 媒体查询
      • 过渡和动画事件
      • 渐进增强和优雅降级
      • CSS3 变形
      • 渐进式渲染
      • CSS 渲染性能优化
      • 层叠上下文
      • CSS3 遮罩
    • JavaScript

      • JavaScript 面试题汇总
      • ✨ let、var、const 的区别
      • JS中的数据类型
      • 包装类型
      • 数据类型的转换
      • 运算符
      • ✨ 原型链
      • ✨ this 指向
      • ✨ 垃圾回收与内存泄漏
      • ✨ 执行栈和执行上下文
      • ✨ 作用域和作用域链
      • ✨ 闭包
      • DOM 事件的注册和移除
      • DOM 事件的传播机制
      • 阻止事件默认行为
      • 递归
      • ✨ 属性描述符
      • class 和构造函数区别
      • 浮点数精度问题
      • 严格模式
      • ✨ 函数防抖和节流
      • ✨ WeakSet 和 WeakMap
      • ✨ 深浅拷贝
      • 函数柯里化
      • Node 事件循环
      • 尺寸和位置
    • 浏览器

      • 浏览器面试题汇总
      • ✨ 浏览器的渲染流程
      • ✨ 资源提示关键词
      • 浏览器的组成部分
      • IndexedDB
      • ✨ File API
      • ✨ 浏览器缓存
      • ✨ 浏览器跨标签页通信
      • Web Worker
    • 网络

      • 网络面试题汇总
      • 五层网络模型 👌
      • 常见请求方法 👌
      • ✨cookie👌
      • 面试题
      • 加密
      • ✨JWT👌
      • ✨ 同源策略及跨域问题 👌
      • 文件上传
      • ✨ 输入 url 地址之后
      • 文件下载
      • ✨ session
      • ✨ TCP
      • ✨ CSRF 攻击
      • ✨XSS 攻击 👌
      • ✨ 网络性能优化
      • 断点续传
      • 域名和 DNS
      • SSL、TLS、HTTPS 的关系
      • ✨ HTTP 各版本差异 👌
      • HTTP 缓存协议
      • ✨ WebSocket
    • 工程化

      • CMJ 和 ESM
      • npx
      • ESLint
    • Vue2

      • Vue 面试题汇总相关
      • 组件通信方式总结
      • 虚拟 DOM
      • v-model
      • 数据响应式原理
      • diff
      • 生命周期详解
      • computed
      • 过滤器
      • 作用域插槽
      • 过度和动画
      • 优化
      • keep-alive
      • 长列表优化
      • 其他 API
      • 模式和环境变量
      • 更多配置
      • 更多命令
      • 嵌套路由
      • 路由切换动画
    • Vue3

      • ✨ Vue3 整体变化 👌
      • ✨ Vue3 响应式变化 👌
      • ✨ nextTick 实现原理 👌
      • 两道代码题 👌
      • Vue 运行机制
      • 渲染器核心功能
      • 事件绑定与更新

模板的本质

  • 渲染函数
  • 模板编译
  • 编译的时机

渲染函数

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

文档地址:

Vue.js

实际上,Vue 里面的单文件组件是会被一个 模板编译器 进行编译的,编译后的结果并不存在什么模板,而是会把模板编译为渲染函数的形式。

这意味着我们完全可以使用纯 JS 来书写组件,文件的内部直接调用渲染函数来描述你的组件视图。

例如我们之前写过的 UserCard 这个组件,完全可以改写成纯 JS 的形式:

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 {
  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;
}

甚至也可以使用 Vue2 经典的 options API 的语法来写:

import styles from './UserCard.module.css'
import { h } from 'vue'
export default {
  name: 'UserCard',
  props: {
    name: String,
    email: String,
    avatarUrl: String
  },
  render() {
    return h(
      'div',
      {
        class: styles.userCard
      },
      [
        h('img', {
          class: styles.avatar,
          src: this.avatarUrl,
          alt: 'User avatar'
        }),
        h(
          'div',
          {
            class: styles.userInfo
          },
          [h('h2', this.name), h('p', this.email)]
        )
      ]
    )
  }
}

至此我们就知道了,Vue 里面之所以提供模板的方式,是为了让开发者在描述视图的时候,更加的轻松。Vue 在运行的时候本身是不需要什么模板的,它只需要渲染函数,调用这些渲染函数后所得到的虚拟 DOM.

作为一个框架的设计者,你必须要思考:你是框架少做一些,让用户的心智负担更重一些,还是说你的框架多做一些,让用户的心智负担更少一些。

模板的编译

单文件组件中所书写的模板,对于模板编译器来讲,就是普通的字符串。

模板内容:

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

对于模板编译器来讲,仅仅是一串字符串:

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

模板编译器需要对上面的字符串进行操作,最终生成的结果:

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

模板编译器在对模板字符串进行编译的时候,是一点一点转换而来的,整个过程:

  • 解析器:负责将模板字符串解析为对应的模板AST
  • 转换器:负责将模板AST转换为 JS AST
  • 生成器:将 JS AST 生成最终的渲染函数

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

假设有这么一段模板:

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

对于模板编译器来讲,就是一段字符串:

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

首先是解析器,拿到这串字符串,对这个字符串进行解析,得到一个一个的 token(编译原理).

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

接下来解析器还需要根据所得到的 token 来生成抽象语法树即模板的AST(以树状结构表示代码语法结构的抽象表示形式)

转换出来的 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"
              }
          ]
        }
      ]
    }
  ]
}

至此解析器的工作就完成了。

接下来就是转换器登场,它需要将上一步得到的模板 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"}
                            ]
                        }
                    ]
                  }
              ]
          }
      }
  ]
}

最后就是生成器,根据上一步所得到的 JS AST,生成具体的 JS 代码:

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

下面是一个模板编译器大致的结构:

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

编译的时机

整体来讲会有两种情况:

  1. 运行时编译
  2. 预编译

1. 运行时编译

例如下面的代码,是直接通过 CDN 的方式引入的 Vue

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .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>
  </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() {
          return {
            name: "John Doe",
            email: "john@example",
            avatarUrl: "./yinshi.jpg",
          };
        },
      }).mount("#app");
    </script>
  </body>
</html>

在上面的例子中,也会涉及到模板代码以及模板的编译,那么此时的模板编译就是在运行时进行的。

2. 预编译

预编译是发生在工程化环境下面。

所谓预编译,指的是工程打包过程中就完成了模板的编译工作,浏览器拿到的是打包后的代码,是完全没有模板的。

这里推荐一个插件:vite-plugin-inspect

npm: vite-plugin-inspect

安装该插件后在 vite.config.js 配置文件中简单配置一下:

// vite.config.js
import Inspect from 'vite-plugin-inspect'

export default {
  plugins: [
    Inspect()
  ],
}

之后就可以在 http://localhost:5173/__inspect/ 里面看到每一个组件编译后的结果。

最近更新:: 2025/7/9 07:29
Contributors: AK
Prev
虚拟DOM本质
Next
组件树和虚拟DOM树