在现今前端项目中,模块化是一个避不开的话题。因此就会出现AMD,CMD等模块加载方式。同时因为JS不停的在更新迭代。出现不少实用的新语法。可是因为有些语法有些超前,JS的宿主环境(浏览器/Node没有跟上JS更新步骤),可是为了在项目中使用这些好用到使人发指的新特性,来提升开发效率等。就出现了各类前端编译插件(Babel)。html
Babel is a JavaScript compiler前端
大多数编译程序(compiler)分为三个步骤:Parsing(分析阶段)/Transformation(转换)/Code Generation(代码生成或者说生成目标代码)node
天然语言 | LISP | C |
---|---|---|
2+2 | (add 2 2) | add(2,2) |
4-2 | (subtract 4 2) | subtract(4,2) |
2 + (4 - 2) | (add 2 (subtract 4 2)) | add(2, subtract(4, 2)) |
Parsing 通常被分红两个步骤:Lexical Analysis(词法分析)和 Syntactic Analysis(语法分析)编程
AST 是一个层级很深的对象。浏览器
e.g. 对(add 2 (subtract 4 2))进行Parsing处理。 Tokens以下(Note:其实Token是根据lexer生成的,不一样的lexer处理结果是不同的。)bash
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' },
]
复制代码
对应的Abstract Syntax Tree (AST) 可能以下编程语言
{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2',
}, {
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4',
}, {
type: 'NumberLiteral',
value: '2',
}]
}]
}]
}
复制代码
transformation是compiler的第二个阶段。他会接收通过SA处理生成的AST。在该阶段可以利用一些语法规则,将AST转换为想被转换的语言。模块化
经过观察AST会发现,每个elements(从AST角度看)或者token(从LA角度看)都有一个type属性。这些element是属于AST的Node结点。这些nodes经过对type属性赋特定的值将AST划分红各自独立的区块。函数
e.g.requirejs
NumberLiteral 类型的Node
{
type: 'NumberLiteral',
value: '2',
}
复制代码
CallExpression 类型的Node
{
type: 'CallExpression',
name: 'subtract',
params: [...内嵌的node逻辑...],
}
复制代码
在transforming AST过程当中,咱们能够经过adding/removing/replacing 属性来修改nodes,同时咱们能够add/remove nodes,甚至咱们能够基于现有的AST来从新构建新的AST对象。
因为咱们是须要将LISP语法的代码转换为C的,因此咱们的关注点就是基于SA输出的AST构建一个全新的适用于目标语言的AST对象。
为了可以在transforming过程当中检测这些nodes。同时因为AST是一个层级很深的对象树,因此须要对AST进行depth-first(深度优先遍历)。(其实这和React在Render阶段是同样的)
{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2',
}, {
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4',
}, {
type: 'NumberLiteral',
value: '2',
}]
}]
}]
}
复制代码
对于上述的AST,在traversal阶段,范围每一个node的前后顺序以下
为了用代码实现traversal过程,咱们构建一个内置可以接收不一样node类型函数的"visitor"对象。
var visitor = {
NumberLiteral() {},
CallExpression() {},
};
复制代码
当在遍历AST的时候,在咱们访问对应的node结点时,就会触发与之类型匹配的visitor中的方法。
若是只是单纯的在访问结点的时候触发对应的方法,这种状况是没法纪录访问的"轨迹",因此须要对visitor进行改进。传入被访问的node结点,还有该node的直接父级结点。
var visitor = {
NumberLiteral(node, parent) {},
CallExpression(node, parent) {},
};
复制代码
若是没有返回处理,"游标"在遍历到最后的node就会中止,由于他不知道下一步该如何进行。
- Program
- CallExpression
- NumberLiteral
- CallExpression
- NumberLiteral
- NumberLiteral
复制代码
因为在遍历AST的过程当中是采用depth-first的方式,就须要在访问到最后的node的时候,须要按照原路返回,直到返回到起点,这样才能被程序识别,这颗树被遍历完成了。
-> Program (enter)
-> CallExpression (enter)
-> Number Literal (enter)
<- Number Literal (exit)
-> Call Expression (enter)
-> Number Literal (enter)
<- Number Literal (exit)
-> Number Literal (enter)
<- Number Literal (exit)
<- CallExpression (exit)
<- CallExpression (exit)
<- Program (exit)
复制代码
为了实现上述逻辑,须要对visitor作额外的处理
var visitor = {
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
},
CallExpression:{
enter(node, parent){},
exit(node, parent){},
},
};
复制代码
compiler的最后阶段是code generation。有些compiler在CG阶段作的工做会和transformation的重叠,可是大部分的CG的工做就是接收被处理过的AST而后将AST对象字符化(该操做相似于JSON.stringify(Object))。 一个高效的CG是可以根据AST不一样的node type输出对应的code,同时可以在树内进行递归调用直到全部的node都被字符化。