8.编译器

编译器源码过于复杂,其中包含过多边界处理和平台构建的代码,直接放上我看的他人解读的链接。

https://juejin.cn/post/6959019076983209992

https://juejin.cn/post/6959019174215548935

https://juejin.cn/post/6960465810682806308

https://juejin.cn/post/6961545472204865572

这篇文章主要是梳理一下脉络

编译器

作用

编译器的核心由三部分组成:

  • 解析 - parse,将类 html 模版转换为 AST 对象
  • 优化 - optimize,也叫静态标记,遍历 AST 对象,标记每个节点是否为静态节点(没有响应式数据的节点),以及标记出静态根节点
  • 生成渲染函数 - generate,将 AST 对象生成渲染函数

图解流程

img

入口与流程

  1. 在 2-Vue初始化过程 中提到,初始化(Vue.prototype._init)的最后一步就是执行 &mount 进行挂载。

在 Vue 全量包中,此时就会进入编译阶段。

而运行时的 Vue 包中,会通过 打包器 结合 vue-loader + vue-compiler-utils 进行预编译,将模版编译成 render 函数。

  1. 在 Vue.$mount 中,只做了一件事情,得到组件的渲染函数,将其设置到 this.$options 上

如果用户提供了 render 配置项,就跳过编译阶段,否则进入编译阶段,解析 template 和 el,转换为 render 函数。在这个过程中会调用很多函数,但是大多数对于用户来说都不重要,它们实在帮助构建平台的配置。其中有一个核心的函数叫 baseCompile.

  1. baseCompile 是核心解析函数

其中调用了三个函数 parse / optimize / generate

parse - 将 html 模板解析成 ast

optimize - 对 ast 树进行静态标记

generate - 将 ast 生成渲染函数,静态渲染函数放到 code.staticRenderFns 数组中,code.render 是动态渲染函数

在将来渲染的时候执行渲染函数得到 vnode

关于 parse / optimize / generate 的细节还是直接看源码和面试题吧

编译前编译后

编译前 html 模板

1
2
3
<div id="app">
<div v-for="item in arr" :key="item">{{ item }}</div>
</div>

编译后 渲染函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
with (this) {
return _c(
'div',
{
attrs:
{
"id": "app"
}
},
_l(
(arr),
function (item) {
return _c(
'div',
{
key: item
},
[_v(_s(item))]
)
}
),
0
)
}

with 语句可以扩展作用域链,所以生成的代码中的 _c、_l、_v、_s 都是 this 上一些方法,也就是说在运行时执行这些方法可以生成各个节点的 vnode。

面试题

简单说一下 Vue 的编译器

Vue 的编译器做了三件事情:

  • 将组件的 html 模版解析成 AST 对象
  • 优化,遍历 AST,为每个节点做静态标记,标记其是否为静态节点,然后进一步标记出静态根节点,这样在后续更新的过程中就可以跳过这些静态节点了;标记静态根用于生成渲染函数阶段,生成静态根节点的渲染函数
  • 从 AST 生成运行时的渲染函数,即大家说的 render,其实还有一个,就是 staticRenderFns 数组,里面存放了所有的静态节点的渲染函数

详细说一说编译器的==解析==过程,它是怎么将 html 字符串模版变成 AST 对象的?

  • 遍历 HTML 模版字符串,通过正则表达式匹配 “<”

  • 跳过某些不需要处理的标签,比如:注释标签、条件注释标签、Doctype。

    备注:整个解析过程的核心是处理开始标签和结束标签

  • 解析开始标签

    • 得到一个对象,包括 标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引位置
    • 进一步处理上一步得到的 attrs 属性,将其变成 [{ name: attrName, value: attrVal, start: xx, end: xx }, …] 的形式
    • 通过标签名、属性对象和当前元素的父元素生成 AST 对象,其实就是一个 普通的 JS 对象,通过 key、value 的形式记录了该元素的一些信息
    • 接下来进一步处理开始标签上的一些指令,比如 v-pre、v-for、v-if、v-once,并将处理结果放到 AST 对象上
    • 处理结束将 ast 对象存放到 stack 数组
    • 处理完成后会截断 html 字符串,将已经处理掉的字符串截掉
  • 解析闭合标签

    • 如果匹配到结束标签,就从 stack 数组中拿出最后一个元素,它和当前匹配到的结束标签是一对。
    • 再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:key、ref、scopedSlot、样式等,并将处理结果放到元素的 AST 对象上
    • 然后将当前元素和父元素产生联系,给当前元素的 ast 对象设置 parent 属性,然后将自己放到父元素的 ast 对象的 children 数组中
  • 最后遍历完整个 html 模版字符串以后,返回 ast 对象

详细说一下静态标记(==优化==)的过程

  • 标记静态节点

    • 通过递归的方式标记所有的元素节点
    • 如果节点本身是静态节点,但是存在非静态的子节点,则将节点修改为非静态节点
  • 标记静态根节点,基于静态节点,进一步标记静态根节点

    • 如果节点本身是静态节点 && 而且有子节点 && 子节点不全是文本节点,则标记为静态根节点
    • 如果节点本身不是静态根节点,则递归的遍历所有子节点,在子节点中标记静态根

什么样的节点才可以被标记为静态节点?

  • 文本节点
  • 节点上没有 v-bind、v-for、v-if 等指令
  • 非组件

为什么要静态标记

优化 patch 函数,静态节点都是不变的,因此在 patch 时应该直接跳过。

Vue3 进一步优化,使用 patchFlags 来标记从未发生改变的节点。

渲染函数的==生成==过程

大家一说到渲染函数,基本上说的就是 render 函数,其实编译器生成的渲染有两类:

  • 第一类就是一个 render 函数,负责生成动态节点的 vnode
  • 第二类是放在一个叫 staticRenderFns 数组中的静态渲染函数,这些函数负责生成静态节点的 vnode

渲染函数生成的过程,其实就是在遍历 AST 节点,通过递归的方式,处理每个节点,最后生成形如:_c(tag, attr, children, normalizationType) 的结果。tag 是标签名,attr 是属性对象,children 是子节点组成的数组,其中每个元素的格式都是 _c(tag, attr, children, normalizationTYpe) 的形式,normalization 表示节点的规范化类型,是一个数字 0、1、2,不重要。

在处理 AST 节点过程中需要大家重点关注也是面试中常见的问题有:

  • 静态节点是怎么处理的

    静态节点的处理分为两步:

    • 将生成静态节点 vnode 函数放到 staticRenderFns 数组中
    • 返回一个 _m(idx) 的可执行函数,意思是执行 staticRenderFns 数组中下标为 idx 的函数,生成静态节点的 vnode
  • v-once、v-if、v-for、组件 等都是怎么处理的

    • 单纯的 v-once 节点处理方式和静态节点一致
    • v-if 节点的处理结果是一个三元表达式
    • v-for 节点的处理结果是可执行的 _l 函数,该函数负责生成 v-for 节点的 vnode
    • 组件的处理结果和普通元素一样,得到的是形如 _c(compName) 的可执行代码,生成组件的 vnode

8.编译器
http://example.com/2022/10/26/8-编译器/
Author
John Doe
Posted on
October 26, 2022
Licensed under