【译】Reading vuejs/core-vapor - 上卷

543 阅读35分钟

前言

vue 有个社区贡献者 ubugeeei(推特/X ID: @ubugeeei)是 vue 官方 Discord 的长期活跃者,经常解答新特性、源码实现相关问题,他对 vue 的源码非常熟悉,也是 Vue Vapor(Vue 无虚拟 DOM 草案)的早期研究者之一,他发布的 “Reading vuejs/core-vapor” 阅读指南站点旨在帮助读者完整梳理 Vapor 模式的编译输出与实现细节,由于 ubugeeei 是一个日本开发者,这个站点只有日语和英语,并且中文互联网上关于这本书的介绍很少,而且我觉得内容很好,因此这里我将这个网站内容翻译成 中文 供大家阅读,由于掘金单篇文章字符限制问题,这里拆分成三部曲进行发布,本篇为上卷

封面.png

开篇之前想和大家聊聊 vue-vapor 的现状

vue-vapor 的原始仓库是本来是 vuejs/vue-vapor ,但是这个仓库目前已声明不再维护,主开发已经迁移到 vuejs/corevapor 分支上,这个分支目前还是非常活跃的,在 vue.js nation 2025 大会上, Evan You 对外展示了 Vue3.6 中的 Vapor Mode 预览,表明核心团队仍在推进该模式向主线合并的努力,大家有兴趣可以去 b 站看 智子 在 vue conf2024 大会上关于 vue-vapor 模式的介绍。

原文链接:ubugeeei.github.io/reading-vue…

介绍

感谢您拿起这本书!本书是了解 Vapor Mode (Vue.js 的下一代实现)实现的指南。虽然它针对的是更高级的读者,但我希望你能找到一些有用的东西,即使只是部分!

本书的目的

  • 深入了解 Vapor ModeVue.js 的下一代实现

  • 加深您对 Vapor Mode 的了解

  • 通过阅读源代码来了解它是如何实现的

本书不涉及的内容

  • 学习如何使用 Vue.js
  • 与其他框架进行比较和评估。

目标读者

  • 想要为 vuejs/corevuejs/core-vapor 做出贡献的人。
  • 想要深化对 Vue.js 理解的人。

关于作者

  • ubugeeei

头像.png

  • Vue.js 成员和 Vue.js 日本用户组的核心成员。

  • 从 2023 年 11 月起参与 Vapor 模式的发展。

  • 2023 年 12 月成为 vuejs/core-vapor 的外部合作者。

  • 2024 年 4 月加入 Vue.js 组织,成为 Vapor 团队的一员。

作者博客:ublog.dev/

Vue Vapor 的实现最初始于一个名为 vuejs/core-vapor 的仓库,但在 2024 年 10 月更名为 vuejs/vue-vapor。在本文档中,链接已更改为 vuejs/vue-vapor,但由于本文档的项目名称和页面,文本已统一为 vuejs/core-vapor。注意,在你阅读时 vuejs/core-vapor = vuejs/vue-vapor。它们在时间线上是不同的名称,但指的是完全相同的事物。

写在前面

什么是 Vue.js?

Vue.js 是一个用户友好、高性能且用途广泛的框架,用于构建 Web 用户界面。

vuejs.org

vue.png

什么是 Vapor 模式?

Vapor ModeVue.js 的下一代实现。由于编译器实现,它提供了一种不使用虚拟 DOM 的模式。我们稍后将更详细地介绍编译器和虚拟 DOM 是什么,主要目标是提高性能

官方 Vue.js 团队在 vuejs GitHub 组织下提供了几个仓库。其中,vuejs/core-vapor 是 Vapor Mode 的实现。这个仓库 vuejs/core-vapor 实际上是 vuejs/core 的一个分支。你可能经常使用的东西,即所谓的 “Vue.js”,在这个 vuejs/core 仓库中。截至 2024 年 9 月,Vapor Mode 仍处于 R&D(研发)阶段,因此尚未合并到 vuejs/core 中。vuejs/core 是 Vue.js v3 开始的实现,而 v2 和更早版本的实现位于一个名为 vuejs/vue 的单独存储库中。(经常有人指出 vuejs/core 的星号较少,但这是因为仓库在过渡到 v3 期间被移动了。在这本书中,我们将专注于 Vapor 模式的实现,这意味着通读 vuejs/core-vapor!Vue.js 实施的相关存储库:

1.png

此外,鉴于此仓库处于研发阶段,它偶尔会以不规则的间隔进行大规模的重构。在本书中,我们将重点介绍截至 2024 年 9 月 16 日的代码。在提交方面,我们将查看第 6,700 次提交之前的源代码,特别是 30583b9ee1c696d3cb836f0bfd969793e57e849d .

2.png

core-vapor 的目录结构

关于仓库术语:在下面的解释中,vuejs/core 和 vuejs/core-vapor 都称之为 v3,例如,“在 v3 仓库中,~~~”

├── packages/
│   ├── compiler-core/
│   ├── compiler-dom/
│   ├── compiler-sfc/
│   ├── runtime-core/
│   ├── runtime-dom/
│   ├── reactivity/
│   ├── compiler-vapor/
│   ├── runtime-vapor/

主要 packages

v3 使用 pnpm workspace 作为 monorepo 进行管理。每个包都位于 /packages 目录中 packages 包

这些包大致分为两类:编译器和运行时。

compiler- 开头的软件包与编译器相关,以 runtime- 开头的软件包与运行时相关。

在 core-vapor 中,添加了新软件包 compiler-vaporruntime-vapor

接下来,一个重要的包是响应性 refcomputedwatch 等实现作为 @vue/reactivity提供,独立于运行时包。它位于 /packages/reactive 中。

用作 Vue.js 入口点的包位于 /packages/vue 中。

除此之外,在 core-vapor 中还添加了一个名为 /packages/vue-vapor 的包,作为 Vapor 模式的入口点。

3.png

compiler-core

顾名思义,compiler-core 提供了 compiler 的核心部分。
除此之外,compiler packages 还包括 compiler-domcompiler-sfc 等。
core 提供了独立于特定用例或环境(如 SFC 或 DOM)的实现。

Vue.js 中有各种 compiler。

例如,当使用 template 选项时,模板在运行时被编译。

createApp({
  template: `<div>{{ msg }}</div>`,
  setup() {
    const msg = ref("Hello, Vue!");
    return { msg };
  },
}).mount("#app");

然而,如你所见,这个模板使用的模板语法与 SFC 中的相同。

<script setup lang="ts">
import { ref } from "vue";

const msg = ref("Hello, Vue!");
</script>

<template>
  <div>{{ msg }}</div>
</template>

此外,还有 HTML 中作为 innerHTML 编写的内容被编译的情况。
Vue.js 有各种编译模板的方式。
理解为 compiler-core 为这些各种用例提供共同部分是大致正确的。

具体来说,它包括将 template 编译成 render 函数的核心实现。

compiler-dom

在 Vue.js 中,与 DOM 相关的操作和代码生成被视为环境依赖,因此与 core 分离。
这在后面的 runtime 部分也会出现。

关于 compiler,它包括生成与 DOM 事件和特定 DOM 元素相关的代码的实现。
如果你考虑 Vue.js 中的事件修饰符,可能会更容易理解。

例如,修饰符 @submit.prevent 需要像这样的代码:

(e: Event) => e.preventDefault()

这是依赖于 DOM API 的代码生成。
提供这种功能是 compiler-dom

示例:

image.png

image.png

compiler-sfc

顾名思义,这是与 SFC(单文件组件)相关的 compiler。
具体来说,它提供了像 <script setup><style scoped> 这样的功能。

在许多情况下,这个 compiler 通过被单独 packages 中的工具(如 bundler)的插件调用来发挥作用。
著名的例子包括 Vite 中使用的 vite-plugin-vuewebpack 中使用的 vue-loader

image.png

image.png

runtime-core

提供 runtime 的核心部分。
同样,它不依赖于 DOM,包括组件 runtime、虚拟 DOM 及其 patching 和 scheduler 的实现。
关于 patching 过程(renderer),虽然看起来可能会执行 DOM 操作,但 runtime-core 只调用定义时不依赖 DOM API 的接口。
实际的函数在 runtime-dom 中实现并注入。(利用依赖倒置原则。)

接口:

image.png

函数 createRenderer 接受实际操作作为选项(不直接在 runtime-core 中调用):

image.png

runtime-dom

包括上述 DOM 操作的实际实现,以及将它们注入到 core 中的实现。

image.png

它还包括实际处理 DOM 事件的实现,如 compiler 解释中提到的。
compiler-dom 是用于输出调用这些的代码的实现。)

image.png

reactivity

顾名思义,提供了 Vue.js 的响应式系统。
你可能在某处听说过"Vue.js 的响应式系统是开箱即用的"。这是因为这个 package 独立实现,不依赖于其他 packages。
此外,它"独立"的事实是 Vapor 模式实现中的一个重要点。

这是有充分理由的。稍微透露一下,Vapor 模式通过利用响应式系统而不使用虚拟 DOM 来更新屏幕。
实际上,对 reactivity package 几乎没有做任何更改。
换句话说,它可以无缝地作为 Vapor 功能的一部分使用,因为它不太依赖于 Vue.js 的 runtime。

compiler-vapor, runtime-vapor

现在,终于到了主题。
顾名思义,这些是 Vapor 模式的 compiler 和 runtime 实现。

Vapor 模式目前处于 R&D 阶段,因此它被实现为独立的 packages,以尽可能避免修改上游中的现有实现。
因此,尽管与现有的 runtime 和 compiler 有很大重叠,但这些部分实际上在这一部分中被重新实现。

这些 packages 中进行了什么样的实现将从这里开始研究(或者说,这是本书的主要主题),所以我们在这里省略。


现在我们对整体 package 结构有了大致了解,让我们开始阅读理解 Vapor 模式实现所必需的源代码!

从哪里开始阅读

在前面的页面中,您应该已经了解了 vuejs/core-vapor 中各个包的概述。
从这里开始,我们将阅读实际的源代码,但应该从哪里开始呢?

答案很简单。

"知己知彼,百战不殆。"

换句话说,"从查看具体输出开始。"
如前所述,Vapor 模式通过编译器的实现提供了一种不使用虚拟 DOM 的模式。

所以,让我们实际编写一个 Vue.js SFC,通过 Vapor 模式编译器运行它,并查看输出。
如果我们能做到这一点,我们可以通过反复进行两个过程来理解 Vapor 模式的实现:"查看产生输出的实现""阅读生成代码的内容"

更详细地分解步骤:

  1. 编写一个 Vue.js SFC
  2. 通过 Vapor 模式编译器运行它
  3. 查看输出(了解概述)
  4. 查看编译器的实现
  5. 阅读输出代码的内容
  6. 返回步骤 1

我们只需要无限重复这个过程。

步骤 1~3 的详细说明

让我详细解释在哪里编写 SFC 以及如何通过 Vapor 模式编译器运行它。

首先,让我们将 vuejs/core-vapor 克隆到您的本地机器上。
然后,检出 30583b9ee1c696d3cb836f0bfd969793e57e849d

vuejs/core-vapor (30583b9ee1c696d3cb836f0bfd969793e57e849d)

git clone https://github.com/vuejs/vue-vapor.git

cd core-vapor

git checkout 30583b9ee1c696d3cb836f0bfd969793e57e849d

pnpm install

当我们阅读 README

This repository is a fork of [vuejs/core](https://github.com/vuejs/core) and is used for research and development of no virtual dom mode.

- [Vapor Playground](https://vapor-repl.netlify.app/)
- [Vapor Template Explorer](https://vapor-template-explorer.netlify.app/)

我们发现了这个描述。

我们注意到有名为 Vapor PlaygroundVapor Template Explorer 的工具。

Playground 是 Vue SFC Playground 的 Vapor 版本。
换句话说,您可以在这里检查编译结果。

4.png

Template Explorer 是一个工具,用于查看 Vapor 模式下生成的代码类型。
实际上,vuejs/core 中有一个原始版本,这是 vuejs/core-vapor 版本。
许多人可能不熟悉它。

它是一个检查 Vue.js 模板(不限于 SFC)编译结果的工具。
因此,您无法看到 SFC 中的样式或脚本是如何转换的。

所以,让我们使用 Playground!或者我想这么说,但有一个小问题。
这次,我们将阅读 30583b9ee1c696d3cb836f0bfd969793e57e849d 的代码,但此链接托管的 Playground 无法修复提交。
由于 Vapor 模式目前处于研发阶段,源代码经常变化。
如果在我们阅读时它发生变化,那将非常不便,所以让我们找一种方法,使用我们刚刚在本地检出的 vuejs/core-vapor 来确认事情。

在 vuejs/core-vapor 中,有一个名为 /playground 的目录。
您可以通过在 vuejs/core-vapor 中运行 pnpm dev 来启动这个 playground。
/playground/src 中放置了一些组件。当您访问启动的 playground 时,您会发现 /playground/src/App.vue 正在您的浏览器中运行。

在这个 playground 中,/playground/src 对应于路由。例如,如果您访问 http://localhost:5173/components.vue,将执行 /playground/src/components.vue
这次让我们利用这个 playground。
现在,让我们重写 App.vue 并查看。
您可以在浏览器开发者工具的源代码选项卡中检查编译结果。

5.png

现在可能看起来很混乱,但不用担心。
我们将从编写较小的 SFC 开始,并逐渐阅读它们。

此外,由于这个 playground 运行的是这个仓库的实现,您可以在进行过程中修改源代码并确认更改。

通过使用这个 playground,我们可以完成步骤 13。
对于步骤 4
6,您可以在阅读本书时跟随!

好了,自上一页以来,介绍已经变得相当长,但现在我们已经准备好阅读源代码了,让我们从下一页开始查看输出和编译器实现!

阅读一个简单的组件

让我们使用 Playground 来阅读最简单的组件。

<template>
  <p>Hello, Vapor!</p>
</template>

输出结果如下。
注意:可能会输出与 HMR 相关的代码,但这次我们将省略它,因为它与我们的主题无关。

const _sfc_main = {};
import { template as _template } from "vue/vapor"; // *注意:由于 Vite 的限制,这实际上指向文件系统上的路径,但我们在这里简化它。*
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}
import _export_sfc from "/@id/__x00__plugin-vue:export-helper";
export default /*#__PURE__*/ _export_sfc(_sfc_main, [
  ["render", _sfc_render],
  ["vapor", true],
  ["__file", "/path/to/App.vue"],
]);

后半部分可能有点混乱,所以大致理解为以下内容。

const _sfc_main = {};
import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}
export default Object.assign(_sfc_main, {
  render: _sfc_render,
  vapor: true,
  __file: "/path/to/App.vue",
});

让我们解释一下。
正如在方法中所解释的,我们应该阅读的是"产生输出的实现"和"生成代码的内容"。步骤是:

  1. 编写一个 Vue.js SFC
  2. 通过 Vapor 模式编译器运行它
  3. 查看输出(了解概述)
  4. 查看编译器的实现
  5. 阅读输出代码的内容
  6. 返回步骤 1

到目前为止,我们已经完成了步骤 1 和 2,所以接下来让我们看一下输出的概述。

理解输出的概述

首先,当您编写一个组件时,您可能会这样做:

export default {
  /* options */
};

或者

export default defineComponent({
  /* options */
});

这将组件对象作为默认导出导出。

在输出代码中:

const _sfc_main = {};
// ...
export default _sfc_main;

这正是那样。
您可以看到 _sfc_render 被设置为这个对象的渲染选项。

_sfc_main.render = _sfc_render;

到目前为止,与传统的 Vue.js 没有太大区别。
现在,让我们看一下 _sfc_render 的内容,这是 Vapor 的核心。

import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}

使用从 vue/vapor 导出的函数 template,我们定义一个模板,生成一个节点,并将其用作渲染函数的结果。
正如您可能从 t0n0 猜到的那样,当我们定义更复杂的模板时,我们将有 t1n1 等。
这些 n0n1 等可以大致理解为 HTMLElement。在这种情况下,一个 p 元素将放在那里。

我想您现在已经理解了输出的概述。


总结一下,编译器将:

<template>
  <p>Hello, Vapor!</p>
</template>

编译成:

const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}

接下来,让我们实际查看编译器的实现。
为此,首先了解编译器实现的概述很重要。

编译器概述

什么是编译器?

将一种代码翻译成另一种代码的实现被称为编译器
在 Vue.js 的情况下,它接收单文件组件作为输入,并输出 JavaScript 和 CSS。

Vapor 模式是 Vue.js 的一种新的编译器实现。
(它输出不使用虚拟 DOM 的代码。)

Vapor 模式编译器的实现主要在 /packages/compiler-vapor 中。

packages/compiler-vapor

这里,我说"主要"是有原因的。

典型的编译器是用"parser"和"generator(codegen)"实现的。
parser分析源代码(字符串)并将其转换为AST(抽象语法树)(对象)。

由于源代码只是一个字符串,解析它并将其视为具有结构的对象可以使未来的转换过程更容易。
该对象就是 AST。

例如,当解析代码字符串:

1 + 2 * 3

它可以表示为:

{
  "type": "BinaryExpression",
  "operator": "+",
  "left": {
    "type": "Literal",
    "value": 1
  },
  "right": {
    "type": "BinaryExpression",
    "operator": "*",
    "left": {
      "type": "Literal",
      "value": 2
    },
    "right": {
      "type": "Literal",
      "value": 3
    }
  }
}

然后,基于获得的 AST,generator生成代码(字符串)。
更准确地说,它将获得的 AST 转换(翻译)成任何形式,并再次将其输出为字符串。

这种"代码(输入)解析 -> 操作 -> 代码(输出)生成"的序列在 Vue.js 中也是相同的。

Vapor 模式编译器的设计

这里的重要点是"Vapor 模式将作为现有单文件组件的子集实现,因此它可以使用现有的parser。"
这意味着 Vapor 的 SFC 没有任何独特的语法。

这个现有的解析器位于 /packages/compiler-sfc/packages/compiler-core 中。
如概述中所解释的,compiler-core 包含模板编译器,而 compiler-sfc 包含 SFC 编译器。
(当然,这些也实现了它们的解析器。)

对应于模板的 AST 的对象称为 AST,对应于 SFC 的 AST 的对象称为 SFCDescriptor

如果我们将到目前为止的讨论图示化,它看起来像这样:

6.png

换句话说,我们按原样使用 compiler-corecompiler-sfc 中的 parserASTSFCDescriptor
每个的具体源代码将在后面介绍。

接下来是 Vapor 特有的部分。Vapor 模式的代码输出部分当然是在 compiler-vapor 中实现的。
这里有一个称为 IR 的新概念。
IR 代表中间表示(Intermediate Representation)。
粗略地说,您可以将其视为"表示输出代码的对象"。
这方面的具体源代码也将在后面介绍。

Vue.js 编译器中的一个重要概念是 transformer
这是一个操作 AST 并转换(转换)它的实现。在 Vapor 模式中,这个转换器主要将 AST 转换为 IR。
然后,基于 IR 生成代码。

(转换器的概念本身并不是 Vapor 独有的;它也在 compiler-core 中实现。然而,Vapor 模式不使用这个,而是使用 compiler-vapor 中实现的转换器。)

这有点复杂,但如果我们再次图示到目前为止的流程,它看起来像这样:

7.png

解析 SFC 和 SFCDescriptor

从这里开始,让我们看看前面解释的每个部分的详细信息。

由于 SFC 的解析器是 SFC 编译器的一部分,它在 compiler-sfc 中实现。

packages/compiler-sfc

SFCDescriptor

首先,解析结果中称为 SFCDescriptor 的对象是一个保存 SFC 信息的对象。
它包括文件名、模板信息、脚本信息、样式信息等。

image.png

模板、脚本和样式都继承自一个称为 SFCBlock 的对象,这个 SFCBlock 包含诸如表示其内容的 content、表示 lang、setup、scoped 等属性的 attrs 以及表示它在整个 SFC 中位置的 loc 等信息。

image.png

template 由一个称为 SFCTemplateBlock 的对象表示,其中包含前面解释的 AST。

image.png

同样,脚本由一个称为 SFCScriptBlock 的对象表示。
这包括一个表示它是否是 setup 的标志,有关正在导入的模块的信息,以及作为块内容的脚本(JS、TS)的 AST。

image.png

同样,样式由一个称为 SFCStyleBlock 的对象表示。

image.png

这大致是 SFCDescriptor 的概述。

如果您实际解析如下的 SFC:

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);
</script>

<template>
  <button type="button" @click="count++">{{ count }}</button>
</template>

那么您将获得如下的 SFCDescriptor
您现在不需要详细阅读 ast。我们稍后会解释它。
注意:省略了一些部分。

{
  "filename": "path/to/core-vapor/playground/src/App.vue",
  "source": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n</script>\n\n<template>\n  <button type=\"button\" @click=\"count++\">{{ count }}</button>\n</template>\n",
  "template": {
    "type": "template",
    "content": "\n  <button type=\"button\" @click=\"count++\">{{ count }}</button>\n",
    "attrs": {},
    "ast": {
      "type": 0,
      "source": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n</script>\n\n<template>\n  <button type=\"button\" @click=\"count++\">{{ count }}</button>\n</template>\n",
      "children": [
        {
          "type": 1,
          "tag": "button",
          "tagType": 0,
          "props": [
            {
              "type": 6,
              "name": "type",
              "value": {
                "type": 2,
                "content": "button",
                "source": "\"button\""
              }
            },
            {
              "type": 7,
              "name": "on",
              "rawName": "@click",
              "exp": {
                "type": 4,
                "content": "count++",
                "isStatic": false,
                "constType": 0,
                "ast": {
                  "type": "UpdateExpression",
                  "start": 1,
                  "end": 8,
                  "operator": "++",
                  "prefix": false,
                  "argument": {
                    "type": "Identifier",
                    "identifierName": "count"
                  },
                  "name": "count"
                },
                "extra": {
                  "parenthesized": true,
                  "parenStart": 0
                },
                "comments": [],
                "errors": []
              }
            },
            "arg": {
              "type": 4,
              "content": "click",
              "isStatic": true,
              "constType": 3
            },
            "modifiers": []
          ],
          "children": [
            {
              "type": 5,
              "content": {
                "type": 4,
                "content": "count",
                "isStatic": false,
                "constType": 0,
                "ast": null
              }
            }
          ]
        }
      ]
    }
  },
  "script": null,
  "scriptSetup": {
    "type": "script",
    "content": "\nimport { ref } from 'vue'\n\nconst count = ref(0)\n",
    "attrs": {
      "setup": true,
      "lang": "ts"
    },
    "setup": true,
    "lang": "ts"
  },
  "styles": []
}

Parser的实现

Parser的实现是下面的 parse 函数。

image.png

image.png

source 包含 SFC 的字符串。
它解析该字符串并返回一个 SFCDescriptor

首先,它使用模板解析器解析整个 SFC。

image.png

compiler 中的 compiler.parse 来自选项,这实际上是 compiler-core 中的模板解析器。

"为什么即使是 SFC 也使用模板解析器...?"

这样想是可以理解的。没错。
但是,如果仔细思考,这是足够的。

在语法上,模板和 SFC 几乎都是 HTML。
在 Vue.js 中解析类似 HTML 的东西时,我们基本上使用 compiler-core 中的解析器。
有一些细微的差异,所以您可以看到我们正在传递 'sfc' 作为 parseMode 参数。

换句话说,compiler-core 处于更一般的位置,而不是专门为模板实现解析器,而 compiler-sfc 中的解析器是它的包装器。

通过这个解析过程,我们可以获得 templatescriptstyle 等的粗略结构,然后我们分支并对每个进行详细解析。

17.webp

在详细部分中,我们处理生成继承自前面 SFCBlock 的每个 Block。
(基本上,我们只是调用一个名为 createBlock 的格式化函数并进行错误处理,所以我们将省略代码。)

之后,我们生成源映射等。

image.png

image.png

至此,这完成了 SFC 的解析过程。

解析模板和 AST

接下来,我们将查看 compiler-core 中实现的解析器和 AST。
SFC 解析器使用这个。

8.png

AST

AST 代表抽象语法树(Abstract Syntax Tree)。

它可能是 Vue.js 编译器拥有的最复杂的中间对象。
指令、插值、插槽等信息在这里表示为 AST。

实现在 compiler-coreast.ts 中。

packages/compiler-core/src/ast.ts

让我们阅读整体结构。

查看节点的类型,我们可以看到有几个类别。

image.png

  • Plain(普通)
  • Containers(容器)
  • Codegen(代码生成)
  • SSR Codegen(服务器端渲染代码生成)

总结一下,codegenssr codegen 与 Vapor 编译器无关。
这与我们稍后将解释的概念有关,例如 IRtransform,但在 Vapor 模式中,代码生成的信息聚合在 IR 中。
然而,在传统的 Vue.js(非 Vapor 模式)中,没有 IR 的概念,甚至输出代码也表示为 AST
在 Vapor 模式中,转换器将 AST 转换为 IR,但在 Vue.js(非 Vapor 模式)中,AST(Plain、Containers)被转换为 AST(Codegen、SSR Codegen)并传递给代码生成器。

这次,为了解释 Vapor 模式编译器的设计,我们不会涉及 codegenssr codegen
让我们看看其他的!

Plain(普通)

首先,没有任何特定类别的基本 AST 节点类型。

image.png

Root(根)

顾名思义,Root 表示模板的根。
它在 children 中有节点。

image.png

Element(元素)

Element 是表示元素的节点。
<p><div> 这样的元素对应于此。
组件和插槽也对应于此。

这些在 children 中也有节点。
它们还有属性信息和指令信息。

image.png

Text(文本)

Text 顾名思义就是文本。
<p>hello</p> 中,hello 对应于此。

image.png

Comment(注释)

Comment 是注释。
<!-- comment --> 对应于此。

image.png

SimpleExpression(简单表达式)

SimpleExpression 是模板中出现的简单表达式。 解释什么是简单的,什么不是简单的有点困难,但例如,ao.a 是简单的,而 (() => 42)() 不是简单的。

{{ foo }} 中的 foo<button @click="handlers.onClick"> 中的 handlers.onClick 对应于此。

image.png

Interpolation(插值)

这是插值语法。
{{ foo }} 对应于此。

image.png

Attribute(属性)

这对应于属性(不是指令)。
<div id="app"> 中,id="app" 对应于此。

image.png

Directive(指令)

这是指令。

v-on:click="handler"v-for="item in items" 对应于此。
当然,像 @click="handler"#head 这样的简写表示法也包括在内。

image.png

Containers(容器)

Containers 是具有特定结构的节点。

image.png

顺序可能有点不连贯,但让我们先看看更容易理解的。

If, IfBranch

IfIfBranch 是由 v-ifv-else-ifv-else 表示的节点。

image.png

结构上,一个 IfNode 有多个 IfBranchNode,一个 IfBranchNode 有一个 condition(条件)和 children(当该条件满足时的节点)。
v-else 的情况下,condition 变为 undefined

For

这是由 v-for 表示的节点。

image.png

image.png

<div v-for="it in list"> 中,source 变为 listvalue 变为 it

CompoundExpression(复合表达式)

这是一个有点难以理解的概念。

compound 意味着"复合",这个节点由多个节点组成。

image.png

{{ foo }} + {{ bar }} 这样的例子对应于此。

直观上,这似乎是 Interpolation + Text + Interpolation 的结构,但
Vue.js 编译器将这些一起视为 CompoundExpression

值得注意的是,在 children 中可以看到 stringSymbol 等类型。

image.png

这是一种机制,为了简单起见,将部分字符串不作为某种 AST 节点而是作为字面量处理。

{{ foo }} + {{ bar }} 之间,字符串的 + 部分更有效地作为字面量 " + " 处理,而不是将其表示为 Text 节点。

它就像以下的 AST:

{
  "type": "CompoundExpression",
  "children": [
    { "type": "Interpolation", "content": "foo" },
    " + ",
    { "type": "Interpolation", "content": "bar" }
  ]
}

TextCall

image.png

这是在将 Text 表示为函数调用 createText 时使用的节点。
现在,您不需要太担心它。


到目前为止,我们已经看了必要的 AST 节点。
从这里开始,让我们看看生成这些 AST 的解析器的实现!

模板解析器

在上一节中,我们详细了解了解析结果 AST。

现在,让我们探索实际生成该 AST 的解析器。

解析器分为两个步骤:parsetokenize

我们从 tokenize 开始。

Tokenize(词法分析)

tokenize 是词法分析步骤。

词法分析是将代码(只是一个字符串)分析成称为标记(词素)的单元的过程。

标记是有意义的字符串块。让我们看看实际的源代码,了解它们是什么。

image.png

实际上,这个标记器是一个名为 htmlparser2 的库的分叉实现。

这个解析器被认为是最快的 HTML 解析器之一,Vue.js 从 v3.4 开始显著提高了性能,通过使用这个解析器。

这在源代码中也有提到:

37.webp

标记在源代码中表示为 State

标记器有一个称为 state 的内部状态,它是 State 枚举中定义的状态之一。

具体来看,我们有默认状态 Text,表示插值开始的 {{,表示插值结束的 }},中间的状态,表示标签开始的 <,表示标签结束的 > 等等。

正如您在以下位置看到的:

image.png

要解析的字符串被编码为 Uint8Array 或数字以提高性能。(我不太熟悉它,但数值比较可能更快。)

由于它是 htmlparser2 的分叉实现,这是否算作阅读 Vue.js 源代码是有争议的,但让我们实际阅读一下Tokenizer的实现。

以下是Tokenizer实现的开始:

export default class Tokenizer {

从构造函数可以看出,为每个标记定义了回调,以实现"标记化 -> 解析"。

(在即将到来的 parser.ts 中,通过定义这些回调来实现模板的解析。)

image.png

image.png

然后,parse 方法是初始函数:

image.png

它将源读取(存储)到缓冲区中,并一次处理一个字符。

this.buffer = input
while (this.index < this.buffer.length) {

它在特定状态下执行回调。

初始值是 State.Text,所以它从那里开始。

image.png

例如,如果 stateText 并且当前字符是 <,它会执行 ontext 回调,同时将 state 更新为 State.BeforeTagName

image.png 通过这种方式,它在特定状态下读取字符,并根据字符类型转换状态,逐步进行。

基本上,这是这个过程的重复。

由于其他状态和字符的实现量很大,我们将省略它们。

(有很多,但它们做的是同样的事情。)

Parse(解析)

现在我们对 Tokenizer 的实现有了一般的了解,让我们继续讨论 parse

这在 parser.ts 中实现。

packages/compiler-core/src/parser.ts

这里使用了我们刚刚讨论的 Tokenizer

const tokenizer = new Tokenizer(stack, {

为每个标记注册回调,以构建模板的 AST。

让我们看一个例子。

请关注 oninterpolation 回调。

顾名思义,这是与 Interpolation 节点相关的处理。

oninterpolation(start, end) {

使用分隔符的长度(默认是 {{}})和传递的索引,它计算 Interpolation 内部内容的索引。

    let innerStart = start + tokenizer.delimiterOpen.length
    let innerEnd = end - tokenizer.delimiterClose.length

基于这些索引,它检索内部内容:

        let exp = getSlice(innerStart, innerEnd)

最后,它生成一个节点:

image.png

addNode 是一个函数,如果存在现有堆栈,则将节点推入其中,否则推入根的子节点。

image.png

stack 是一个堆栈,元素在嵌套时被推入其中。

既然我们在这里,让我们也看看那个过程。

当一个开放标签完成时——例如,如果它是 <p>,在 > 的时机——当前标签被 unshift 到堆栈中:

image.png

stack.unshift(currentOpenTag!)

然后,在 onclosetag 中,它移动堆栈:

image.png

通过这种方式,通过充分利用 Tokenizer 回调,构建了 AST。

虽然实现量很大,但我们基本上只是稳步地进行这些过程。

Vapor 模式中的 IR

现在,让我们继续讨论 IR
从这里开始,我们将深入研究 Vapor 模式的具体实现。

9.png

我们将首先查看 IR,然后再阅读 transformer 的源代码。

什么是 IR?

IR 代表中间表示(Intermediate Representation)。
虽然 SFCDescriptorAST 本质上是用户(Web 应用程序开发者)输入代码的结构化版本,但 IR 可以被视为"输出代码的结构化版本"。
IR 的定义可以在 ir/index.ts 中找到。

packages/compiler-vapor/src/ir/index.ts

回想一下我们在开始时阅读的小组件的编译器输出:

import { template as _template } from "vue/vapor";
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}

直接从 AST 输出这些有些困难。
作为一种策略,我们可以准备一个对象(IR)来表示上面的代码,通过操作 AST 生成 IR,然后将该 IR 传递给代码生成器,从而以编程方式设计编译器。

让我们实际看看上面的组件会产生什么样的 IR。
我们可以通过在本地编译器中插入日志来检查这一点。

在以下区域附近有一个 transform 函数,所以让我们在转换后输出 ir

image.png

{
  "type": 0,
  "node": {
    "type": 0,
    "source": "\n  <p>Hello, Vapor!</p>\n",
    "children": [
      {
        "type": 1,
        "tag": "p",
        "ns": 0,
        "tagType": 0,
        "props": [],
        "children": [
          {
            "type": 2,
            "content": "Hello, Vapor!"
          }
        ]
      }
    ],
    "helpers": {},
    "components": [],
    "directives": [],
    "hoists": [],
    "imports": [],
    "cached": [],
    "temps": 0
  },
  "source": "\n  <p>Hello, Vapor!</p>\n",
  "template": ["<p>Hello, Vapor!</p>"],
  "component": {},
  "directive": {},
  "block": {
    "type": 1,
    "node": {
      "type": 0,
      "source": "\n  <p>Hello, Vapor!</p>\n",
      "children": [
        {
          "type": 1,
          "tag": "p",
          "ns": 0,
          "tagType": 0,
          "props": [],
          "children": [
            {
              "type": 2,
              "content": "Hello, Vapor!"
            }
          ]
        }
      ],
      "helpers": {},
      "components": [],
      "directives": [],
      "hoists": [],
      "imports": [],
      "cached": [],
      "temps": 0
    },
    "dynamic": {
      "flags": 1,
      "children": [
        {
          "flags": 1,
          "children": [
            {
              "flags": 1,
              "children": []
            }
          ],
          "id": 0,
          "template": 0
        }
      ]
    },
    "effect": [],
    "operation": [],
    "returns": [0]
  }
}

这是实际的 IR。
由于 IR 中的类型表示为枚举,它们显示为数字,使其有点难以理解。让我们用特定的 IR 节点名称替换它们,并删除不必要的部分。
然后它变成如下内容:

{
  "type": "RootIRNode",
  "node": {
    "type": "RootNode",
    "source": "\n  <p>Hello, Vapor!</p>\n",
    "children": [
      {
        "type": "ElementNode",
        "tag": "p",
        "ns": 0,
        "tagType": 0,
        "children": [
          {
            "type": "TextNode",
            "content": "Hello, Vapor!"
          }
        ]
      }
    ],
    "temps": 0
  },
  "source": "\n  <p>Hello, Vapor!</p>\n",
  "template": ["<p>Hello, Vapor!</p>"],
  "block": {
    "type": "BlockIRNode",
    "node": {
      "type": "ElementNode",
      "source": "\n  <p>Hello, Vapor!</p>\n",
      "children": [
        {
          "type": "ElementNode",
          "tag": "p",
          "ns": 0,
          "tagType": "Element",
          "children": [
            {
              "type": "TextNode",
              "content": "Hello, Vapor!"
            }
          ]
        }
      ],
      "temps": 0
    },
    "returns": [0]
  }
}

首先,根部有一个 RootIRNode。这是 IR 的根。
这个 RootIRNode 包含诸如 nodetemplateblock 等信息。
node 是 AST 的 RootNode

image.png

然后,block 包含一个 BlockIRNode,它表示 Vapor 中处理的元素单元 Block

block: BlockIRNode

image.png

在这里,让我们解释一下 Block

什么是 Block?

Block 是 Vapor 模式中处理的单元。
它类似于非 Vapor 模式中的 VNode(虚拟 DOM 节点)。

Block 的定义在 runtime-vapor 中,所以让我们看一下。

image.png

看了这个,你可以了解 Block 是什么。
Block 接受一个 Node(DOM 节点)、一个 Fragment、一个 Component 或一个 Block 数组。
基本上,Vapor 使用这个称为 Block 的单元构建 UI。

例如,

const t0 = template("<p>Hello, Vapor!</p>");
const n0 = t0();

这里,n0 成为一个 Block,它是一个 Node(Element)。
当我们解释运行时时,我们将更详细地查看这一点,但让我们简要地看一下 template 函数。

image.png

它只是将模板插入 innerHTML 并返回其 firstChild
换句话说,这只是一个 ElementNode

有时它是一个 Element,有时是一个 Component,或者有时由这些的数组组成。Block 是用于构建 UI 的最小单元。
在未来,诸如注册事件监听器或更新文本之类的操作将在这个 Block 上执行。

从这里开始,我们将在阅读各种组件时阅读每个 IR 的定义,所以我们将在这里结束对 IR 的解释。
目前,了解 IR 是什么以及我们在开始时阅读的小组件在 IR 中是如何表示的一般概念就足够了。

transformer 概述

实现

接下来,让我们看看将 AST 转换为 IRTransformer

10.png

正如我们在 compiler 概述中讨论的,transformer 的概念自 vuejs/core 以来就存在于 compiler-core 中。实现大约在这里。

由于它与 Vapor 模式无关,我们这次将跳过它,但 Vapor 模式中的 transformer 是参考原始 transformer 设计的(未使用)。
我们这次要阅读的 Vapor 模式的 transformer 实现大约在这里。

我们在 compiler 中调用 transform.ts 中实现的 transform 函数。

image.png

调用顺序(compile: parse -> transform -> generate):

image.png

 const ast = isString(source) ? parse(source, resolvedOptions) : source

image.png

return generate(ir, resolvedOptions)

Transformer 的设计

Transformer 中有两种类型的接口。 NodeTransformDirectiveTransform

image.png

image.png

各种 transformer 在 /transforms/ 中实现,它们是这两种类型之一。

快速总结一下哪个是哪个:

正如你可能从名称中猜到的那样。 这些 transformer 将 AST 转换为 IR。

从以下内容可以看出

image.png

这些 transformer 作为选项传递给 transform 函数。

nodeTransformsdirectiveTransforms 来自以下内容:

const [nodeTransforms, directiveTransforms] =
  getBaseTransformPreset(prefixIdentifiers)

image.png

image.png

阅读 transform 函数

让我们直接阅读 transform 函数。

image.png

transform 函数持有一个名为 TransformContext 的单一对象。

简而言之,它是一个持有转换所需选项和状态的对象。

export class TransformContext<T extends AllNode = AllNode> {

当我们跟随实际的转换过程时,我们将在这个上下文中阅读实现。

现在,我们通过将这个上下文传递给一个名为 transformNode 的函数来开始转换过程。

  const context = new TransformContext(ir, node, options)
    
  transformNode(context)

image.png

这次,我们将跟踪从我们当前正在阅读的小组件获得的 AST 的转换过程。

<template>
  <p>Hello, Vapor!</p>
</template>

获得的 AST 如下:

{
  "type": "RootNode",
  "source": "\n  <p>Hello, Vapor!</p>n",
  "children": [
    {
      "type": "ElementNode",
      "tag": "p",
      "ns": 0,
      "tagType": "Element",
      "props": [],
      "children": [
        {
          "type": "TextNode",
          "content": "Hello, Vapor!"
        }
      ]
    }
  ],
  "helpers": {},
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": [],
  "temps": 0
}

首先,这个 Node 进入 transformNode,然后 transformNode 按顺序执行作为选项传递的 nodeTransforms

image.png

根据设计,应用 transform 后,任何要在最后执行的函数都作为 onExit 接收。这些被存储起来以便稍后执行,

image.png

并在 transformNode 结束时执行。

image.png

让我们直接看看 nodeTransforms 的执行。顺序如下:

image.png

由于我们这次还没有使用指令或插槽,我们将按顺序阅读 transformText -> transformElement -> transformChildren

transformText

实现在这里。

packages/compiler-vapor/src/transforms/transformText.ts

如果我们正在查看的 nodetypeELEMENT,并且它的所有子节点都是类文本的并且包含插值,我们将该节点视为"文本容器"并处理它(processTextLikeContainer)。类文本节点是文本或插值。

image.png

这次,正如你从 AST 中看到的,

{
  "type": "ElementNode",
  "tag": "p",
  "ns": 0,
  "tagType": "Element",
  "props": [],
  "children": [
    {
      "type": "TextNode",
      "content": "Hello, Vapor!"
    }
  ]
}

由于我们这次不包含插值,我们不会进入这个分支。

虽然顺序有点不连贯,但我们继续一个接一个地读取节点,当我们进入 TextNode 时,我们通过更下面的以下分支。

  } else if (node.type === NodeTypes.TEXT) {
   context.template += node.content
  }

我们将文本节点的内容添加到上下文的 template 属性中并完成。template 变成 "Hello, Vapor!"

transformElement

实现在这里。

packages/compiler-vapor/src/transforms/transformElement.ts

首先,这个 transform 完全在 onExit 生命周期内操作。
注意它返回一个函数。

 return function postTransformElement() {

由于这次不是 Component,transformNativeElement 将被执行(假设我们现在正在读取 p 标签)。

const isComponent = tagType === ElementTypes.COMPONENT

image.png

image.png

transformNativeElement 中,我们生成一个字符串作为 template 函数的参数传递。

首先,我们从 AST 中提取标签名并将其与 < 连接。

  let template = ''

  template += `<${tag}`

如果有 props,我们也会生成这些,但由于这次没有,我们将跳过它。

最后,我们插入 context 中持有的 childrenTemplate 并生成闭合标签来完成。

image.png

childrenTemplate 是在 transformChildren 中创建的。

就 transforms 的执行顺序而言,它是 transformText -> transformElement -> transformChildren,但由于我们刚刚看到的 transformElement 处理是在 onExit 中执行的,所以 transformChildren 先执行,因此 childrenTemplate 已经生成。

现在让我们看看 childrenTemplate 实际上是在哪里创建的。

transformChildren

实现在这里。

packages/compiler-vapor/src/transforms/transformChildren.ts

它做的事情很简单:它按顺序对传入 node 的每个 children 执行 transformNode

image.png

这里有趣的是,当它进入子节点时,它首先专门为子节点创建一个新的上下文(childContext)。
然后,在 transformNode 完成后,它检索该 childContext 中持有的 template 并将其推入父 context
(推入只是 Array.prototype.push

    } else {
      context.childrenTemplate.push(childContext.template)
    }

我们已经能够在 context.template 中创建字符串 "<p>Hello, Vapor!</p>"

尚未完成

虽然我们能够生成字符串,但实际上我们需要生成如下代码:

const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0();
  return n0;
}

我们仍然缺少一些信息来实现这一点。
我们还没有看到将这个模板变成 t0,将结果分配给 n0,并在 render 函数中返回它的实现。
我们将在下一页看到这是在哪里完成的。

transformer 概述 2

因为内容有点长,所以我分开了,这里是续篇。
虽然被称为概述,但在我意识到之前,我已经在阅读特定实现的细节,不过,也许这是一个很好的自然引入(笑)。

在上一页中,我们看了生成作为 template 函数参数传递的模板字符串的实现。
在这里,我们将看看 Block 的索引管理等。

registerTemplate

TransformContext 中有一个名为 registerTemplate 的函数。

image.png

这个 registerTemplate 调用一个名为 pushTemplate 的函数。

image.png

模板(字符串)被注册到 this.ir.template(数组)中。

this.ir 是一个 RootIRNode

  constructor(
   public ir: RootIRNode,

也就是这里。

image.png

而且,这个 registerTemplate 在三个地方被调用。

  1. transformNode 结束时(仅当根节点时)
  if (context.node.type === NodeTypes.ROOT) {
    context.registerTemplate()
  }
  1. transformChildren 中处理子节点后(仅当它是 Fragment 时)
    if (isFragment) {
     childContext.reference()
     childContext.registerTemplate()
  1. 当调用 context.enterBlock 时(onExit

image.png

context.enterBlock 是在 transformVFortransformVIf 中进入 Block 时调用的函数。
当我们看到编译 v-forv-if 的实现时,我们会看到这个,但现在,只需掌握 1 和 2 就可以了。

关于 1,我认为没有什么特别的。
在我们现在正在阅读的小组件中,模板在这里注册。
换句话说,此时,this.ir.template 处于 ["<p>Hello, Vapor!</p>"] 状态。

如果我们有这个,我们就知道模板的索引,所以,

const t0 = template("<p>Hello, Vapor!</p>");

看起来我们可以生成这样的代码。(我们实际上会在 codegen 期间再次看到这个。)

在情况 2 中,Fragment,是当我们编写如下模板时:

<template>
  <p>Hello, Vapor 1</p>
  <p>Hello, Vapor 2</p>
  <p>Hello, Vapor 3</p>
</template>

在这种情况下,三个模板在时机 2 注册。

// this.ir.template
["<p>Hello, Vapor 1</p>", "<p>Hello, Vapor 2</p>", "<p>Hello, Vapor 3</p>"];

const t0 = template("<p>Hello, Vapor 1</p>");
const t1 = template("<p>Hello, Vapor 2</p>");
const t2 = template("<p>Hello, Vapor 3</p>");

render 函数的返回值

回到关于

<template>
  <p>Hello, Vapor!</p>
</template>

的讨论,让我们再次回顾从中获得的 IR
(省略了不必要的部分)

{
  "type": "RootIRNode",
  "template": ["<p>Hello, Vapor!</p>"],
  "block": {
    "type": "BlockIRNode",
    "returns": [0]
  }
}

仔细看,有一个相当可疑的叫做 "returns": [0] 的东西。
从这个信息中,我们可以理解索引为 0 的节点似乎是 render 函数的返回值。

这是在 transformChildren 中完成的。

image.png

在某些条件下,该节点的 id 被推入 block.returns
这个 id 是在调用 pushTemplate 时从长度计算的。

image.png

而且,

在某些条件下

至于这些条件是什么,第一个条件是当 isFragmenttrue 时。
这是当执行 transformChildrennodeRootElementTemplateComponent 之一时。

image.png

第二个条件是当 dynamic.flags 不是 NON_TEMPLATE 或是 INSERT 时。
(注意:乍一看可能有点混乱,但由于它是一个位掩码,每个标志不是互斥的。)

      !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
      childContext.dynamic.flags & DynamicFlag.INSERT

当这两个条件都满足时,id 被推入 block.returns
我认为第一个条件没问题。
关于第二个条件,关于 dynamic.flags

dynamic.flags

dynamicTransformContext 的一个属性。

dynamic: IRDynamicInfo = this.ir.block.dynamic

image.png

由于 context.ir 持有这个信息,它的引用被保存在 TransformContext 中。
特别是,这次,IRDynamicInfo 持有的名为 DynamicFlag 的信息很重要,所以让我们专注于这个。

DynamicFlag 是表示节点具有什么样属性的标志。
属性如注释中所述。

image.png

由于它是用位掩码表示的,每个属性可以共存。

让我们看看每个标志何时被标记。

DynamicFlag.REFERENCED

这个节点被引用,需要保存为变量。

如所述。

有两个地方设置了 DynamicFlag.REFERENCED

  1. 当调用 context.reference 时。

image.png

  1. 当通过 newDynamic 生成 IRDynamicInfo 时(作为默认值)

image.png

首先,在情况 1 中,context.reference 在很多地方被调用。
例如,它在我们之前看到的 transformChildrenisFragmenttrue 的条件分支中被调用。

    if (isFragment) {
     childContext.reference()
     childContext.registerTemplate()

然后,关于这个标志的用途,它是在生成代码时生成 id 的。
当我们看到 codegen 的实现时,我们会详细看到这一点,但设置了这个标志的节点将生成一个 id 并将其存储在变量中。

需要保存为变量。

如所述。

可以理解在 transformChildren 中的 isFragment 中设置这个标志。
通过这样做,

const n0 = t0();
const n1 = t1();
const n2 = t2();

我们可以输出持有像 n${id} 这样的变量的代码。
相反,不需要存储在变量中的节点不会设置这个标志。

在这种情况下,

const t0 = template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0(); // 这里
  return n0;
}

由于我们需要在变量中持有 n0,所以设置了这个标志。

DynamicFlag.NON_TEMPLATE

接下来是 DynamicFlag.NON_TEMPLATE
是否设置这个标志非常重要;如果没有设置,id 将被推入 block.returns

image.png

这个节点不是从模板生成的,而是动态生成的。

如所述,这个标志似乎是为不是从模板生成而是动态生成的节点设置的。

例如,这个标志在 transformComponentElementtransformSlotOutlettransformVFor 等中设置。

79.webp

跳过一点,与这个标志结合使用的一个重要点是 DynamicFlag.INSERT

DynamicFlag.INSERT

是否将 id 推入 returns 首先通过检查 DynamicFlag.NON_TEMPLATE 是否未设置来确定。
如果未设置,此时将其推入 returns

如果已设置,我们检查 DynamicFlag.INSERT 是否已设置。

看一下

80.webp

你可以看到 ComponentSlotOutletv-for 从一开始就设置了这个标志。

然而,

81.webp

此时没有设置这个标志。

关于 if,在 v-if(不是 v-else-ifv-else)的情况下,设置了这个标志。

  if (dir.name === 'if') {
   const id = context.reference()
   context.dynamic.flags |= DynamicFlag.INSERT

而在像 <template #foo> 这样的插入插槽的情况下,不设置这个标志。

image.png

通过这种方式,它们选择推入 block.returns 的内容和不推入的内容。


在我们的小组件的情况下,由于没有设置 DynamicFlag.NON_TEMPLATE 标志,id 被推入 block.returns。 有了这个,我们已经生成(转换)了 codegen 所需的所有 IR

接下来,让我们看看 codegen 的实现!

Codegen 概述

到目前为止,我们已经了解了解析代码、生成 AST 以及通过 transformer 将其转换为 IR 的过程。
最后,让我们看看代码生成(codegen),它从 IR 生成代码。
通过理解这一点,您将对编译器有一个实质性的了解。

11.png

实现位置

代码生成器(generator)的实现可以在以下区域找到:

结构与转换器类似;generate.ts 实现了 generate 函数和 CodegenContext,而 generators 目录包含了每个节点的代码生成函数。

image.png

export class CodegenContext {

像往常一样,让我们在跟随组件的代码生成时根据需要阅读 CodegenContext

generate

首先,让我们进入 generate 函数。

83.webp

代码使用从 buildCodeFragment 获得的 push 函数按顺序附加。

  const [frag, push] = buildCodeFragment()

image.png

首先,我们推送渲染函数的签名。

84.webp

然后,我们使用 genBlockContent 从 IR 生成代码。

  push(INDENT_START)
  push(...genBlockContent(ir.block, context, true))
  push(INDENT_END, NEWLINE)

由于模板的声明和导入语句是在渲染函数之外完成的,这些被生成为 preamble 并添加到代码的开头。

85.webp

这个 code 成为最终代码。

image.png

现在,让我们阅读 genBlockContent

关于 _sfc_render

在检查输出代码时,渲染函数的名称是 _sfc_render 而不是 render
然而,在 generate 函数中,它被推送为 render

实际上,render 函数后来被 vite-plugin-vue 的实现重写为 _sfc_render
因此,名称 _sfc_render 实际上并不出现在 compiler-vapor 中。

image.png

genBlockContent

实现位于 packages/compiler-vapor/src/generators/block.ts

image.png

我们从 block.dynamic.children 中取出每个子项并生成代码。

  for (const child of dynamic.children) {
   push(...genChildren(child, context, child.id!))
 }

block.dynamic.childrentransformChildren 中生成,其内容直接包括 childContext.dynamic

context.dynamic.children[i] = childContext.dynamic

再次查看除了标志之外还包括哪些信息:

image.png

我们可以看到它包括诸如 id 和模板索引之类的信息。
使用这些信息,我们用 genChildren 生成代码。

genChildren

genChildrenpackages/compiler-vapor/src/generators/template.ts 中实现。

image.png

这个函数生成像 const n${id} = t${template}() 这样的代码。
在这种情况下,它生成像 const n0 = t0() 这样的代码。

image.png

这里,也生成了像 nextSiblingfirstChild 这样稍后会出现的代码。(现在可以跳过这个。)

继续 genBlockContent

一旦生成了子项的代码。

接下来,我们生成操作和效果。
这些还没有出现,但它们涉及为文本更新和事件处理程序注册等生成代码。

  push(...genOperations(operation, context))
  push(...genEffects(effect, context))

最后,我们生成 return 语句。

我们映射 block.returns,生成像 n${idx} 这样的标识符,并生成 return 语句。

image.png


至此,代码生成就是这样。
现在,我们已经能够跟踪编译简单组件所需的编译器实现。
让我们总结我们的目标、步骤和剩余任务,并继续下一步!

最后

中卷的内容是SFC Compilation,以及阅读运行时,插值语法绑定,复杂模板,调度器;大家要是感兴趣可以关注这系列的文章,码字不易,喜欢还请点个赞支持下