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. Towers CodeForces - 229D

    The city of D consists of n towers, built consecutively on a straight line. The height of the tower ...

  2. Jacobi symbol(裸雅可比符号)

    Jacobi symbol Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Tot ...

  3. .NET Core跨平台的奥秘[上篇]:历史的枷锁

    微软推出的第一个版本的.NET Framework是一个面向Windows桌面和服务器的基础框架,在此之后,为此微软根据设备自身的需求对.NET Framework进行裁剪,不断推出了针对具体设备类型 ...

  4. Android 开发笔记___SD卡基本操作

    package com.example.alimjan.hello_world; /** * Created by alimjan on 7/5/2017. */ import android.ann ...

  5. Android 开发笔记___初级控件之实战__计算器

    功能简单,实现并不难,对于初学者可以总和了解初级控件的基本使用. 用到的知识点如下: 线性布局 LinearLayout:整体界面是从上往下的,因此需要垂直方向的linearlayout:下面每行四个 ...

  6. C#递归查询

    一.sql --构造测试数据: 只作演示用 CREATE TABLE [dbo].[Tim_LinqTable]( [Id] int PRIMARY KEY IDENTITY(1,1) NOT NUL ...

  7. Java中的Redis应用

    1.配置redis集群   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding ...

  8. 快速自检电脑是否被黑客入侵过(Windows版)

    我们经常会感觉电脑行为有点奇怪, 比如总是打开莫名其妙的网站, 或者偶尔变卡(网络/CPU), 似乎自己"中毒"了, 但X60安全卫士或者X讯电脑管家扫描之后又说你电脑" ...

  9. 初始MyBatis

    初始MyBatis 框架的概念: 框架是一个提供可重复的功用结构的半成品.它为我们构建新的应用程序提供了极大的便利,一方面提供了可以拿来就用的工具,更重要的是提供了可重用的设计.D 框架技术的优势: ...

  10. MySQL plugin结构

    1.背景 MySQL插件安装语法如下: 13.7.3.3 INSTALL PLUGIN Syntax INSTALL PLUGIN plugin_name SONAME 'shared_library ...