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 是天然的树形结构,树形图想必诸位早就脑补出来了吧:

源代码生成的抽象语法树

以上是使用 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源码系列(03):生成render函数

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

  2. 大白话Vue源码系列(04):生成render函数

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

  3. 大白话Vue源码系列(03):生成AST

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

  4. 大白话Vue源码系列(05):运行时鸟瞰图

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

  5. 大白话Vue源码系列(01):万事开头难

    阅读目录 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源码 系列一(前言-目录篇)

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

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

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

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

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

随机推荐

  1. linux DHCP安装和测试

    1.Yum 安装DHCP服务 2.拷贝模板配置文件,方便后期的配置修改. cp /usr/share/doc/dhcp-4.1.1/dhcpd.conf.sample /etc/dhcp/dhcpd. ...

  2. 小米Java程序员第二轮面试10个问题,你是否会被刷掉?

    近日,开发者头条上分享了一篇"小米java第二轮面经",有很多的java程序员表示非常有兴趣. 下面l就和各位分享小米java第二轮面经(华为java工程师笔试面试题可以看文章某尾 ...

  3. spa(单页应用)中,使用history模式时,微信长按识别二维码在ios下失效的问题

    spa(单页应用,vue)中,使用history模式时,微信长按识别二维码在ios下失效的问题. 触发条件: spa单页应用: 路由模式 history 从其他页面跳转到带有微信二维码识别的页面(不是 ...

  4. jquery IE6 下animate 动画的opacity无效

    jquery IE6 下animate 动画的opacity无效,其实是有效的,因为IETester的一个小BUG 原生IE6 没问题...呵呵~~

  5. NFS服务器的安装与配置

    由于实验室的项目需要实现在CephFS之上建立NFS之上,所以记录一下NFS服务器的安装与配置流程. 1.NFS服务的简介: NFS 是 Network File System 的缩写,是Sun公司于 ...

  6. Python 3.X 调用多线程C模块,并在C模块中回调python函数的示例

    由于最近在做一个C++面向Python的API封装项目,因此需要用到C扩展Python的相关知识.在此进行简要的总结. 此篇示例分为三部分.第一部分展示了如何用C在Windows中进行多线程编程:第二 ...

  7. [转载] HDFS简介

    转载自http://www.csdn.net/article/2010-11-26/282582 http://subject.csdn.net/hadoop/ 一.HDFS的基本概念 1.1.数据块 ...

  8. 工作中用到的一些shell命令

    1.将十进制转换为十六进制 for i in `seq 0 127`; do printf "%02x\n" $i; done

  9. eclipse项目中丢失的R包找回方法

    当我们项目中的R文件丢失的时候会令我们痛苦不已,怎样找回呢?总不能删了吧,那样心血会毁于一旦的,我们肯定不会那样做,那要怎么办呢?我这里提供三种方法: ​一,一般情况下这样: ​    ​方法一:选中 ...

  10. iOS上new Date异常解决办法

    最近有一个项目要实现使用Angluar写一个简历模板, 用户输入姓名/生日/简介...等内容, 然后生成一份在线的简历 后来测试时遇到简历模板在Android手机跟Google浏览器上根据生日计算得出 ...