看 babel doc 的时候,首页看到了一个链接,说是能从 high level 解释 babel 是怎么工作的 !!!
以下是babel官网提供的链接中的大致内容,有兴趣的戳这里看原文
简单说明
编译 是从 源代码(通常为高级语言) 到能直接被计算机编译器或虚拟机执行的 目标代码(通常为低级语言或机器语言) 的翻译过程.
如何用 JS 写一个超级小巧 (200行代码预定) 的编译器.
我们即将 (通过这个编译器) 把一些看着像 lisp 的代码, 编译成类 C 代码.
其实和把 ES6 转为 ES5, JSX/TS 转为 JS 一个套路.
具体的例子就是这个样子
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))
非常简单吧?
虽然这既不是 LISP 语法也不是 C 语法. 但足够展示现代编译器的主要部分了.
背景
大多数编译器分为三个主要阶段:解析,转换,代码生成.
- 解析 : 将 原始代码 转换为 更抽象的表示。
- 转换 : 将这个 抽象的表示 转化为编译器想要的东西.
- 代码生成 : 将转化后的表示 转化为新的代码.
解析
解析通常分为两个阶段:词法分析 和 句法分析
1 词法分析 (Lexical Analysis)
获取 原始代码 并通过 标记化器tokenizer(词法分析器lexer) 将其 拆分为 tokens
tokens 是一组非常小的 object,描述语法中的一个独立的部分。它们可能是数字,标签,标点符号,运算符, 等等等等。
2 语法分析 (Syntactic Analysis)
获取 token 并将其重新格式化为 描述语法的各个部分及其关系 的一种表现形式。这种表现形式称为 中间语言(intermediate representation) , 也叫 抽象语法树。
比如:
(add 2 (subtract 4 2))
这其中的 tokens 大概会是:
[
{ 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',
}]
}]
}]
}
这样看起不爽, 其实 body 中的内容就是
add
/ \
2 substract
/ \
4 2
转换
将 AST 转化为同样的语言, 或者其他完全不同的语言.
看看怎么转化 AST 吧.
你可能注意到了, 我们的 AST 中有一些元素, 看起来都差不多.
都有一个 type 属性. 这些我们把它叫做 AST 节点.
这些节点定义了自身的属性.
比如说这是一个数字:
{
type: 'NumberLiteral',
value: '2',
}
这是一个调用表达式:
{
type: 'CallExpression',
name: 'add',
params: [ ...一些内嵌节点... ]
}
转换就是对 AST 节点的操作, 我们增加,删除,替换里面的属性.
也可以增加新的节点, 删除节点.
我们也可以把这个 AST 扔在这不操作, 而是基于它建一个新的出来.
因为我们这里, 要把代码转换为另一种语言(LIST to C), 我们后面就建一个新的 AST 好了.
遍历
为了浏览节点,我们需要能够遍历他们。
此遍历过程将使用 深度优先 来访问 AST 中的每个节点.
对于这个 AST.
Program
|
add
/ \
2 substract
/ \
4 2
我们遍历的顺序是:
- Program
- CallExpression (add)
- NumberLiteral (2)
- CallExpression (substract)
- NumberLiteral (4)
- NumberLiteral (2)
如果我们要直接操作这棵树的话, 介绍一下不同的遍历方式还是有用的,
然而我们只是建一颗新的, 所以深度优先遍历足够了.
访客 vistor
基本思路是: 我们创建一个访客 vistor 对象, 它有不同方法能接受不同类型 (type) 的节点.
var visitor = {
NumberLiteral() {},
CallExpression() {},
};
遍历过程中, 当我们进入 (enter) AST 中每一个节点的时候, 我们都会根据节点 type 调用这个 vistor 的对应方法.
为了弄清楚类型, 这个函数需要接收当前节点 node
为了遍历, 这个函数需要知道它的父节点 parent
var visitor = {
NumberLiteral(node, parent) {},
CallExpression(node, parent) {},
};
不过, 我们可能还需要考虑一下 "退出 exit"
看一下树:
- Program
- CallExpression
- NumberLiteral
- CallExpression
- NumberLiteral
- NumberLiteral
当我们遍历时, 可能走到没有子节点的分支, 我们就该 exit 了,
所以向下遍历我们叫做 "enter", 往回退我们叫做 "exit".
-> 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)
为了支持 enter 和 exit, 最终我们的 vistor 差不多是这样:
var visitor = {
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
}
};
代码生成
最后一步啦.
有一些 compiler 可能会把 transform 和这一步叠加在一起,
但是 代码生成(code generation) 这一步的主要目的是, 把我们的AST转为代码字符串(string-ify)。
代码生成有很多种做法, 大多数就是用我们刚才生成的 AST 来做的.
我们的 code generator 应该知道怎么打印出这些不同类型的 node (递归 nodes 是常见操作)
ta-da!
完毕, 这就是编译器的所有不同部分.
并不是说每个编译器看起来都像我在这里描述的那样.编译器有许多不同的用途,它们可能需要更多的步骤.
你现在应该大概知道它是怎么运作的.
代码时间
点这里 获取代码
(/ ^ ▽ ^ )/ tokenizer
我们将从解析第一阶段,词法分析 tokenizer (token 化)开始.
也就是把代码字符串, 转化为一组 token
(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]
function tokenizer(input) {
// current 标记我们现在在 code 的哪个位置, 其实就是 index
let current = 0;
let tokens = [];
while (current < input.length) {
let char = input[current];
// 左右括号
if (char === '(') {
tokens.push({
type: 'paren',
value: '(',
});
current++;
continue;
}
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 空格
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 数字
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
let value = '';
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'number', value });s
continue;
}
// 也处理一下 string 字符串
if (char === '"') {
let value = '';
char = input[++current];
while (char !== '"') {
value += char;
char = input[++current];
}
char = input[++current];
tokens.push({ type: 'string', value });
continue;
}
// 处理函数名
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'name', value });
continue;
}
// 识别不出来
throw new TypeError('I dont know what this character is: ' + char);
}
}
ヽ/❀o ل͜ o\ノ parser
接收一个 token 数组, 返回一个 AST
[{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }
// 接收一个 token 数组
function parser(tokens) {
let current = 0;
// 递归 recursion
function walk() {
let token = tokens[current];
if (token.type === 'number') {
current++;
// 数字节点, 直接返回
return {
type: 'NumberLiteral',
value: token.value,
};
}
if (token.type === 'string') {
current++;
// 字符串节点, 直接返回
return {
type: 'StringLiteral',
value: token.value,
};
}
// 遇到一个左括号, 调用表达式
if (
token.type === 'paren' &&
token.value === '('
) {
// 创建一个调用节点
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
token = tokens[++current];
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
// 遇到右括号之前, 在这个节点下遍历, 都放入调用参数中.
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
// AST 根节点
let ast = {
type: 'Program',
body: [],
};
while (current < tokens.length) {
// 遍历所有节点, 添加进去
ast.body.push(walk());
}
return ast;
}
the token array
[
{ 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: ')' }, //<<< Closing parenthesis
{ type: 'paren', value: ')' }, //<<< Closing parenthesis
]
the tree
(
/
param
/ | \
"add" 2 (
|
param
/ | \
"sub" 4 2
AST DONE!!
⌒(❀>◞౪◟<❀)⌒ traverser
使用 visitor 访问不同 AST 节点, 在遇到不同 type 的节点 call 不同的方法.
函数结构:
traverse(ast, {
Program: {
enter(node, parent) {},
exit(node, parent) {}
},
CallExpression: {
enter(node, parent) {},
exit(node, parent) {},
},
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
},
})
定义一个 traverser 方法接收一个 AST 和 visitor. 内部有两个 functions
// 定义一个 traverser 方法接收一个 AST 和 visitor. 内部有两个 functions
function traverser(ast, visitor) {
// `traverseArray` 可以让我们遍历数组, 并调用我们定义的另一个方法 `traverseNode`
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// `traverseNode` 接收 `node` 和它的 `parent` 节点.
// 这样我们就能传 visitor 的两个 methods.
function traverseNode(node, parent) {
// 我们首先通过 type 看看 visitor 是否存在方法
let methods = visitor[node.type];
// 如果这个节点有 `enter` 方法, 我们就 enter 进入
if (methods && methods.enter) {
methods.enter(node, parent);
}
// 接下来,我们将按节点 type 拆分
switch (node.type) {
// 从 level `Program` 开始, 它的 body 属性的值是一个 node 的 array,
// 我们用上面的方法遍历
// traverseArray 将依次调用 `traverseNode`,所以我们在通过递归对树进行遍历
case 'Program':
traverseArray(node.body, node);
break;
// 接下来, 我们对 CallExpression 的参数 params 做同样的事
case 'CallExpression':
traverseArray(node.params, node);
break;
// 遇到没有子节点的 `NumberLiteral` 和 `StringLiteral`,
// 直接 break 就可以了
case 'NumberLiteral':
case 'StringLiteral':
break;
// 不认识的节点, 直接报错
default:
throw new TypeError(node.type);
}
// 如果这个节点有 `exit` 方法, 我们就 exit 回退
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 调用 traverseNode, 传入 AST, AST 的根节点没有 parent 传 null
return traverseNode(ast, null);
}
⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽ the transformer !!!
接下来,transfromer。
我们的 transformer 将基于我们刚刚建立的 AST 创建一个新的 AST.
----------------------------------------------------------------------------
Original AST Transformed AST
----------------------------------------------------------------------------
{ | {
type: 'Program', | type: 'Program',
body: [{ | body: [{
type: 'CallExpression', | type: 'ExpressionStatement',
name: 'add', | expression: {
params: [{ | type: 'CallExpression',
type: 'NumberLiteral', | callee: {
value: '2' | type: 'Identifier',
}, { | name: 'add'
type: 'CallExpression', | },
name: 'subtract', | arguments: [{
params: [{ | type: 'NumberLiteral',
type: 'NumberLiteral', | value: '2'
value: '4' | }, {
}, { | type: 'CallExpression',
type: 'NumberLiteral', | callee: {
value: '2' | type: 'Identifier',
}] | name: 'subtract'
}] | },
}] | arguments: [{
} | type: 'NumberLiteral',
| value: '4'
| }, {
| type: 'NumberLiteral',
| value: '2'
| }]
| }
| }
| }]
| }
----------------------------------------------------------------------------
所以, 我们的 transformer接收 lisp 的 AST
function transformer(ast) {
let newAst = {
type: 'Program',
body: [],
};
ast._context = newAst.body;
// 用 AST 和 visitor 调用 traverser
traverser(ast, {
// 第一个 visitor 接收所有的 `NumberLiteral`
NumberLiteral: {
// 通过 enter 访问
enter(node, parent) {
// 我们会创建一个新的节点, 也叫做 NumberLiteral, 然后把它放入 parent context
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 接下来是 `StringLiteral`
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// `CallExpression`.
CallExpression: {
enter(node, parent) {
// 创建一个新节点 `CallExpression` 并内嵌一个 `Identifier`.
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// 接下来我们在原来的 `CallExpression` 节点上,
//定义一个 context 指向 expression 的参数
node._context = expression.arguments;
// 如果父节点不是 `CallExpression`
if (parent.type !== 'CallExpression') {
// 用 `ExpressionStatement` 包一下 `CallExpression`,
//因为 `CallExpression` 在 JS 中是真的语句
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
// 最后, 把 (包装过的) `CallExpression` 放入 parent 的 context.
parent._context.push(expression);
},
}
});
// 返回刚刚生成的 new AST
return newAst;
}
ヾ(〃^∇^)ノ♪ code generator
最后一步, Code Generator, 将以递归的方式打印上一步得到的 AST 的每个节点.
function codeGenerator(node) {
// 通过node 的 type 属性, 把 AST 转为 code
switch (node.type) {
// 如果遇到 Program 节点, 就遍历 body 中的每个节点,
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 对于 `ExpressionStatement`, 我们在内嵌表达式中递归的使用代码生成器, 结束后加上分号
case 'ExpressionStatement':
return (
codeGenerator(node.expression) + ';'
);
// 如果遇到 `CallExpression`, 就打印 `callee` 和左括号,
// 把 node 中的 arguments 映射成数组, 用逗号 join 一下, 最后打印一个右括号
case 'CallExpression':
return (
codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')'
);
// 遇到 `Identifier` 直接返回 `node`'s 名字.
case 'Identifier':
return node.name;
// 对于 `NumberLiteral` 直接返回 `node` 的值.
case 'NumberLiteral':
return node.value;
// 对 `StringLiteral` 给 `node` 的值加上引号.
case 'StringLiteral':
return '"' + node.value + '"';
// 对于其他节点, 报错
default:
throw new TypeError(node.type);
}
}
点这里获取代码.
后面准备逐一撕一下前端这些库, 框架的面具, 都拾掇拾掇, 折腾折腾. 加油.
今天就到这里了.