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 对象生成渲染函数
图解流程
入口与流程
- 在 2-Vue初始化过程 中提到,初始化(Vue.prototype._init)的最后一步就是执行 &mount 进行挂载。
在 Vue 全量包中,此时就会进入编译阶段。
而运行时的 Vue 包中,会通过 打包器 结合 vue-loader + vue-compiler-utils 进行预编译,将模版编译成 render 函数。
- 在 Vue.$mount 中,只做了一件事情,得到组件的渲染函数,将其设置到 this.$options 上
如果用户提供了 render 配置项,就跳过编译阶段,否则进入编译阶段,解析 template 和 el,转换为 render 函数。在这个过程中会调用很多函数,但是大多数对于用户来说都不重要,它们实在帮助构建平台的配置。其中有一个核心的函数叫 baseCompile.
- baseCompile 是核心解析函数
其中调用了三个函数 parse / optimize / generate
parse - 将 html 模板解析成 ast
optimize - 对 ast 树进行静态标记
generate - 将 ast 生成渲染函数,静态渲染函数放到 code.staticRenderFns 数组中,code.render 是动态渲染函数
在将来渲染的时候执行渲染函数得到 vnode
关于 parse / optimize / generate 的细节还是直接看源码和面试题吧
编译前编译后
编译前 html 模板
1 |
|
编译后 渲染函数
1 |
|
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