目前的前端项目里,如果不是那些上古世纪留下的代码,最新的项目大家应该都选择vue或者react开发,然后运用各种炫酷的ES6语法,开发起来那叫一个爽。
在使用ES6语法的时候,我们需要引用的一个插件就是babel,
大家应该对babel不陌生,但是,你真的知道它背后的原理吗? 今天我们就来探索一下,让我们使用E6飞起的背后的男人:babel
一些概念
在了解babel背后的原理之前,我们先来聊聊babel背后的一些概念
什么是babel
Babel 是一个 JavaScript 编译器,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript
比如下面的代码:
[1,2,3].map(n => n + 1);
经过babel编译之后
[1, 2, 3].map(function (n) {
return n + 1;
});
上面的代码中,babel经历了以下三个过程:
解析(parse)--> 转换(transform)--> 生成(generate)
中间的那个,叫抽象语法树(AST)
AST
代码解析成AST的目的是为了方便计算机更好地理解我们的代码,那么什么是AST?
官方定义:
看完一脸懵逼,我们还是来看个例子吧在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。
const a = 1 + 3
经过转换后为
{
"type": "Program",
"start": 0,
"end": 15,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 15,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 15,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "a"
},
"init": {
"type": "BinaryExpression",
"start": 10,
"end": 15,
"left": {
"type": "Literal",
"start": 10,
"end": 11,
"value": 1,
"raw": "1"
},
"operator": "+",
"right": {
"type": "Literal",
"start": 14,
"end": 15,
"value": 3,
"raw": "3"
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
通过例子,我们就比较好理解什么是AST了,它是上面代码中const a = 1 + 3
的树状表现形式
type: xxx,
start: xx
end: xx
上面代码的结构,叫做节点(Node)。一个AST是由多个或单个这样的节点组成,节点内部可以有多个这样的子节点,构成一颗语法树,这样就可以描述用于静态分析的程序语法。 节点中的type字段表示节点的类型,比如上述AST中的"VariableDeclarator"、"Identifier"、"Literal"等等,当然每种节点类型会有一些附加的属性用于进一步描述该节点类型。
babel的运行原理
理解了上面的一些概念,接下来我们来了解一下babel的运行原理
第一步:解析(parse)
很多js引擎都有自己的AST解析器,比如谷歌的v8引擎。而Babel是通过Babylon实现的。
在解析过程中有两个阶段:词法分析和语法分析
词法分析阶段:
要做词法分析,我们首先需要知道,在js中,哪些属于语法单元:
- 数字:JavaScript 中的科学记数法以及普通数组都属于语法单元.
- 括号:『(』『)』只要出现,不管任何意义都算是语法单元
- 标识符:连续字符,常见的有变量,常量(例如: null true),关键字(if break)等
- 运算符:+、-、*、/等等
- 一些注释,中括号等
我们在解析代码的过程中,可以将代码理解为一段字符串,词法分析阶段把字符串形式的代码转换成令牌(tokens)流
令牌(tokens)流又是什么鬼?
你可以把它看作是一个扁平化的语法片段数组,类似于AST中的节点
如:1+3 代码经过词法分析转换成令牌流
// 1+3
[
{ type: { ... }, value: "1", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "+", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "3", start: 4, end: 5, loc: { ... } },
...
]
每个type都有一组属性来描述该令牌:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
语法分析
语法分析阶段则会把一个令牌流转换成AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。
语法分析比词法分析要复杂,因为要分析各种语法的可能性,需要开发者根据令牌流提供的信息来分析出代码之间的逻辑关系,只有经过词法分析令牌流才能成为有结构的抽象语法树. 语法分析一般会依照一个标准,大多数 JavaScript Parser 都遵循estree规范
第二阶段:转换(transform)
通过第一步的解析,babel将js代码解析成了AST,在第二个阶段,babel接受AST并通过babel-traverse对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是Babel插件介入工作的部分,此时,得到的是新的AST树。
听起来起来有点抽象,举个例子
比如我们要用 babel 做一个Vue转小程序的转换器, babel工作流程的粗略情况是这样的:
babel 将 vue 代码解析为AST抽象语法树,开发者利用 babel 插件定义转换规则,根据原本的抽象语法树生成一个符合小程序规则的新抽象语法树, babel 则根据新的抽象语法树生成代码,此时的代码就是符合小程序规则的新代码
例如 Taro就是用 babel 完成的小程序语法转换.
我们转换代码的关键就是根据当前的抽象语法树,以我们定义的规则生成新的抽象语法树,转换的过程就是生成新抽象语法树的过程.
vue代码输入 ==》 babylon进行解析 ==》 得到AST ==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树 ==》 用babel-generator通过AST树生成符合小程序使用的代码
插件就是应用于babel的转译过程,如果这个阶段不使用任何插件,那么babel会原样输出代码。
babel官方帮我们做了一些预设的插件集,称之为preset,这样我们只需要使用对应的preset就可以了。以JS标准为例,babel提供了如下的一些preset:
es2015
es2016
es2017
...
第三步:生产(generate)
将经过转换的AST通过babel-generator
再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。同时还会创建源码映射(source maps)。
手写一个简单的babel
通过前面的文章,我们知道了babel的解析过程为3步走策略
第一步,由Babylon
进行解析为AST
第二步由babel-traverse
转义为新的AST
第三步,由babel-generator
生成解析后的代码。
下面,我们试着将const a = 1 + 3
,通过我们写的babel,生成为var a = 1 + 3
import * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";
const code = `const a = 1+3 ;`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
if (path.node.type === "VariableDeclaration" && path.node.kind === 'const' ) {
path.node.kind = 'var';
}
}
})
const genCode = generate(ast, {}, code);
console.log('genCode', genCode); // {code: "var a = 1 + 3;", map: null, rawMappings: null}
一些babel插件的书写也是这个原理,本文粗略探讨了babel的原理以及解析过程,更加详细的内容,可以查看这里:传送门