大白话Vue源码系列(02):编译器初探

时间:2022-09-26 09:43:21

Vue 的编译器模块相对独立且简单,本篇就从这块入手,先把它干掉。

编译器代码入口文件

前面已经提到,Vue 项目中的 entry-runtime.js 文件是 Vue 用于构建 仅包含运行时 的源码文件,而 entry-runtime-with-compiler.js 是用于构建 同时包含编译器和运行时 的全功能文件。因此两个文件的差集必然就是编译器实现。

先看一下 entry-runtime.js 文件的内容:

import Vue from './runtime/index'

export default Vue

文件里总共就这两行代码。这样的话就基本确定编译器相关的代码就在 entry-runtime-with-compiler.js 文件里了,事实证明也确实是这样。

Vue.prototype.$mount

entry-runtime-with-compiler.js 文件里的关键代码是为 Vue 的 prototype 扩展了一个 $mount 方法,并将模板编译相关的工作都封装在了这个 $mount 方法里。

在具体深扒 $mount 方法的内部实现之前,有必要先看一下它的应用场景是怎样的,这样会更有助于理解它内部是怎么工作的。

例如下面一段 html 模板:

<div id="index">
<div>{{msg}}</div>
</div>

开发者可以通过如下操作使用 Vue 将上面这段模板编译成 render 函数:

let vm = new Vue({
data: {
msg: 'hello',
}
}); // 实例化 Vue 时 new Vue(options) 传入的 options 可通过 vm.$options 访问
console.log(vm.$options.render);
/* Console 输出:
* undefined
*/ vm.$mount('#index'); console.log(vm.$options.render);
/* Console 输出:
* ƒ anonymous() {
* with(this){return _c('div',{attrs:{"id":"index"}},[_c('div',[_v(_s(msg))])])}
* }
*/

可以看到在调用 $mount 方法之后已经生成了 Vue 的 render 函数。

更常用也更方便的用法是:

new Vue({
el: '#index',
data: {
msg: 'hello',
},
});

这两种写法是完全等价的。实际上,如果在实例化 Vue 的时候提供了 el 选项,Vue 也是在内部调用 $mount 方法进行编译的。

接下来就看看 $mount 方法的具体是怎么实现的,为了更加清晰地描述思路,以下均使用伪代码进行书写:

/**
* 作用:将 Vue 的 html 模板编译成 render 函数。
*
* 通过将 $mount 方法定义在 Vue 的 prototype 上,
* 使得每一个 new 出来的 Vue 实例都能使用 $mount 方法。
*/
Vue.prototype.$mount = function (el){
// options 是 new Vue(options) 提供的实参 options
const options = this.$options; // 优先使用实例化 Vue 时提供 render 函数
if (options.render) {
// 已经是 render 函数了,因此不用做任何操作
return this; // 如果没有提供 render 函数,则优先使用提供的 template 选项
}else if(options.template){
template = getOuterHTML(options.template); // 如果既没有提供 render 函数,又没有 template 选项,就使用 el 选项
}else{
template = getOuterHTML(el);
} // 编译 html 模板生成 render 函数,并赋给 options 的 render 选项
// 这也是为什么上面在调用 $mount 方法之后 vm.$options.render 的值发生了变化
options.render = compileToFunctions(template); return this;
} // 负责兼容多样化的输入形式并返回要处理的 html模板片段
function getOuterHTML(){/*...*/}
// 负责将 html模板片段编译成 render 函数
function compileToFunctions(el){/*...*/}

可以看到,如果实例化 Vue 的时候同时提供了 rendertemplateel 选项中的多个,则 Vue 使用的优先级是 render > template > el

# getOuterHTML 函数

上面的 getOuterHTML 函数所做的工作就是兼容你使用 Vue 的各种姿势,比如:

  • { el: '#index' }
  • { el: document.querySelector('#index') }
  • { template: '#index' }
  • { template: '<div>{{msg}}</div>'}

你可以传 CSS 选择器,也可以直接传 DOM, 还可以传 html 片段,怎么玩你说了算。getOuterHTML 函数的返回值是 DOM 的 outerHTML,总之,它负责得到 html 模板片段

至此一切仍然是在扯淡,上面的都只是前戏,现在还没进入真正的编译阶段。眼贼的同学估计已经看到了,上面的 compileToFunctions 函数才是真刀实枪负责编译的。

# compileToFunctions 函数

接下来就扒进去看看 compileToFunctions 是怎么把 getOuterHTML 获得的 html 模板片段编译成 render 函数的。

compileToFunctions 函数编译模板的过程主要分为三步:

  1. 将 html 模板解析成抽象语法树(AST)。
  2. 对 AST 做优化处理。
  3. 根据 AST 生成 render 函数。

什么是抽象语法树

抽象语法树(Abstract Syntax Tree) 是源代码语法结构的抽象表示,并以树这种数据结构进行描述。AST 属编译原理范畴,有比较成熟的理论基础,因此被广泛运用在对各种程序语言(JavaScript, C, Java, Python等等)的编译处理中。Vue 同样也是使用 AST 作为中间形式完成对 html 模板的编译。

构建 AST 的一般过程

首先看一下第一步,也就是 解析成 AST。但是在继续 Vue 模板如何生成 AST 之前,有必要先看一下 AST 的一般解析过程。

通常程序语言解析成 AST 的过程会分为两步:

  1. 词法分析(Lexical Analysis)
  2. 语法分析(Syntax Analysis)

拿咱最熟悉的 JavaScript 来说吧,比如下面一段程序:

let a = 1

词法分析器会把代码的字符序列转换为单词序列(tokens)。经过词法分析后就能得到如下一个词素列表:

[
{ type: 'Keyword', value: 'let' },
{ type: 'Identifier', value: 'a' },
{ type: 'Punctuator', value: '=' },
{ type: 'Numeric', value: '1' }
]

语法分析器会在词法分析的基础上将单词序列(tokens)组合成各类语法短语(语句、表达式等)。经过语法分析后即可得到 AST 的 JSON 格式:

{
type: "Program",
body: [
{
type: "VariableDeclaration",
declarations: [
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "a"
},
init: {
type: "Literal",
value: 1,
raw: "1"
}
}
],
kind: "let"
}
],
sourceType: "script"
}

上面的英文单词大家不认识的自己去搜下翻译哈。JSON 是天然的树形结构,树形图想必诸位早就脑补出来了吧:

大白话Vue源码系列(02):编译器初探

源代码生成的抽象语法树

以上是使用 Esprima 工具对 JS 代码进行词法分析和语法分析的结果。

这里有一个 在线的AST生成工具

还有一个 AST树形图预览工具

Vue 构建的 AST

扯了这么多,应该对抽象语法树有个模糊的概念了吧,这对理解 Vue 的 AST 构建过程就足够用了。

回到正题,Vue 的 html 模板比较特殊,因为它根本算不上是一门语言,而是基于 HTML 的声明式绑定。因此,Vue 生成的 AST 类似于大家已经非常熟悉且非常成熟的 DOM 树,实际上 Vue 也确实是仿照着 DOM 树进行解析的。只要你熟悉 DOM 树,Vue 生成的 AST 是灰常好看且简单的。如果连 DOM 树都不了解,那咱只能帮你到这里了,你一定是个假前端。

最后再次强调的一点是,Vue 编译器的编译结果是一个函数——Vue 的 render 函数,AST 只是方便处理的中间形式

本篇完,将在下篇深究 Vue 构建 AST 的细节。

大白话 Vue 源码系列目录

本系列会以每周一篇的速度持续更新,喜欢的小伙伴记得点关注哦。

大白话Vue源码系列(02):编译器初探的更多相关文章

  1. 大白话Vue源码系列&lpar;03&rpar;:生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  2. 大白话Vue源码系列&lpar;04&rpar;:生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  3. 大白话Vue源码系列&lpar;03&rpar;:生成AST

    阅读目录 AST 节点定义 标签的正则匹配 解析用到的工具方法 解析开始标签 解析结束标签 解析文本 解析整块 HTML 模板 未提及的细节 本篇探讨 Vue 根据 html 模板片段构建出 AST ...

  4. 大白话Vue源码系列&lpar;05&rpar;:运行时鸟瞰图

    阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ...

  5. 大白话Vue源码系列&lpar;01&rpar;:万事开头难

    阅读目录 Vue 的源码目录结构 预备知识 先捡软的捏 Angular 是 Google 亲儿子,React 是 Facebook 小正太,那咱为啥偏偏选择了 Vue 下手,一句话,Vue 是咱见过的 ...

  6. 大白话Vue源码系列目录

    .first-level{ font-size: 1.2rem; cursor: default; color: #666; } .second-level{ font-size: 1.1rem; p ...

  7. 手牵手,从零学习Vue源码 系列一(前言-目录篇&rpar;

    系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 手牵手,从零学习Vue源码 系列三(虚拟DOM篇) 陆续更新中... 预计八月中旬更新 ...

  8. 手牵手,从零学习Vue源码 系列二(变化侦测篇)

    系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...

  9. Vue源码学习02 初始化模块init&period;js

    接上篇,我们看到了VUE分了很多模块(initMixin()stateMixin()eventsMixin()lifecycleMixin()renderMixin()),通过使用Mixin模式,都是 ...

随机推荐

  1. 初识 Sql Server存储过程

    开篇语 之前的公司并未使用存储过程来做项目,所以小生对存储过程的调用.使用也是一知半解,刚好这家公司就大量用到了存储过程 这次做的功能,为了保持风格一致,也是需要使用存储过程来实现动态sql和数据分页 ...

  2. Redis到底该如何利用?

    Redis是个好东西,经过上两个星期的研究和实践,目前正在项目里大规模的替换掉原来的本地内存cache.但是替换过程中却发现,Redis这东西高端,大气上档次,似乎不是我想象里的使用方法. 在没有深入 ...

  3. &lbrack;codevs1155&rsqb;&lbrack;KOJ0558&rsqb;&lbrack;COJ0178&rsqb;&lbrack;NOIP2006&rsqb;金明的预算方案

    [codevs1155][KOJ0558][COJ0178][NOIP2006]金明的预算方案 试题描述 金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间.更让他高兴 ...

  4. Apache Shiro权限框架在SpringMVC&plus;Hibernate中的应用

    在做网站开发中,用户权限必须要考虑的,权限这个东西很重要,它规定了用户在使用中能进行哪 些操作,和不能进行哪些操作:我们完全可以使用过滤器来进行权限的操作,但是有了权限框架之后,使用起来会非常的方便, ...

  5. scala学习笔记(四)样本类与模式匹配

    访问修饰符 格式:private[x]或protected[x],x指某个所属包.类或单例对象,表示被修饰的类(或方法.单例对象),在X域中公开,在x域范围内都可以访问: private[包名]:在该 ...

  6. makefile debug

    1. 使用warning指令 warning 是个不错的命令,可以打印出消息,来判断makefile执行的流程 2.使用ifeq ifneq 当makefile被多次调用到的时候,如果都输出warni ...

  7. java的配置环境简介

    ============================================================================== 学java对很多人来说并不陌生,听的最多的 ...

  8. Core Graphics 和Quartz 2D的区别

    quartz是一个通用的术语,用于描述在IOS和MAC OS X中整个媒体层用到的多种技术 包括图形.动画.音频.适配.  Quart 2D 是一组二位绘图和渲染API,Core Graphic会使用 ...

  9. 200行Python代码实现2048

    200行Python代码实现2048 一.实验说明 1. 环境登录 无需密码自动登录,系统用户名shiyanlou 2. 环境介绍 本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到桌面 ...

  10. STS启动springboot项目,加载不了resources下的配置文件的问题

    从这篇博客的评论中找到了解决方案 答案: eclipse的设置中,它默认是不包括resources下的文件的,把它改了就行了 原本用idea没这些事的,不过idea旗舰版到期了,社区版的话,对前端又没 ...