什么是 babel?
babel 是一个 JavaScript compiler。
babel 可以做以下几个事情:
- 转换语法
- polyfill 目标环境中缺少的功能(例如通过 core-js 实现的第三方 polyfill)
- 源代码转换
- 解析 jsx
- 解析 flow 和 TypeScript
babel 有以下几个特点:
- 可插件化
- 可调试(支持 Source Map)
- 规范性(遵循 ECMAScript)
- 可压缩(使用尽可能少的代码而不依赖庞大的运行时环境)
AST
要想弄清楚 babel 的原理之前,首先要搞清楚 AST 的概念。
我们需要了解 babel 是如何将 JavaScript 代码生成 AST的,babel 又是如何操作 AST 的。
分析 AST 代码可以查看: https://astexplorer.net/
babel 的工作流程
大多数的编译器工作流程基本分为三个部分:
- Parse(解析):将源码转换成更加抽象的表示方法 (AST)
- Transform(转换):将 AST 进行特殊处理,让它复合编译器的期望
- Generator (代码生成):将第二步得到转换过的 AST 生成新的代码
如:ES6 -> ES5:
- Parse:得到 ES6 AST
- Transform:得到 ES5 AST
- Generator:生成 ES5 Code
1、Parse (解析)
一般来说,Parse 阶段可以细分为两个阶段:词法分析(Lexical Analysis, LA)和语法分析(Syntactic Analysis, SA)。
词法分析
词法分析阶段可以看成对代码进行分词,它接收一段源代码,然后执行一段 tokenize 函数,把代码分割成 Token
。Token 是一段数组,可以通过:https://esprima.org/demo/parse.html# 生成。
var answer = 6 * 7;
如上述代码,拆分成的 Token 数组为:
[
{
"type": "Keyword", // 关键字
"value": "var"
},
{
"type": "Identifier", // 标识符
"value": "answer"
},
{
"type": "Punctuator", // 标点符号
"value": "="
},
{
"type": "Numeric", // 数值
"value": "6"
},
{
"type": "Punctuator",
"value": "*"
},
{
"type": "Numeric",
"value": "7"
},
{
"type": "Punctuator",
"value": ";"
}
]
具体 tokenize 函数可以参考这段代码:https://github.com/babel/babel/tree/master/packages/babel-parser/src/tokenizer
语法分析
词法分析后,代码就已经变成一个 Tokens 数组了,现在需要通过语法分析吧 Tokens 数组转化为 AST。
具体 parser 可以参考:https://github.com/babel/babel/tree/master/packages/babel-parser/src/parser
{
"type": "Program", // AST 的根节点一定是 Program
"body": [
{
"type": "VariableDeclaration", // 变量声明
"declarations": [ // 声明
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier", // 标识符
"name": "answer"
},
"init": {
"type": "BinaryExpression", // 二项式
"operator": "*",
"left": {
"type": "Literal", // 词法
"value": 6,
"raw": "6"
},
"right": {
"type": "Literal",
"value": 7,
"raw": "7"
}
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
2、Transform(转换)
具体转换的规范可以参考:https://github.com/estree/estree
Babel 对于 AST的遍历是深度优先遍历,对于 AST 上的每一个分支,Babel 都会先向下遍历走到尽头,然后再向上遍历退出刚才遍历过的节点,然后寻找下一个分支。
如上述代码,从 declarations 开始遍历:
- 声明了一个变量,并且我们已经知道了它的内部属性:(id、init)然后我们再依次访问它每一个属性及子节点
- id 的类型是 Identifier,表示这是一个标识符,它的名字是 answer
- init 也有几个属性:(type、operator、left、right)
- type 为 BinaryExpression 表示这是一个二项式
- operator 操作符为 *
- left 表示左值,值为 6
- right 表示右值,值为 7
Babel 会维护一个称作 Visitor 的对象,这个对象定义了用于 AST 中获取具体节点的方法。
Visitor
一个 Visitor 一般来说是这样的:
var visitor = {
ArrowFunction() {
console.log('我是箭头函数');
},
IfStatement() {
console.log('我是一个if语句');
},
CallExpression() {}
};
当我们遍历 AST 的时候,如果匹配上一个 type,就会调用 visitor 里的方法。
这只是一个简单的 Visitor。
上面说过,Babel 遍历 AST 其实会经过两次节点:遍历的时候和退出的时候,所以实际上 Babel 中的 Visitor 应该是这样的:
var visitor = {
Identifier: {
enter() {
console.log('Identifier enter');
},
exit() {
console.log('Identifier exit');
}
}
};
该细节可以参考:https://github.com/babel/babel/tree/master/packages/babel-traverse
3、generate (生成)
经过 transfor 阶段,需要转译的代码已经经过转换生成新的 AST 了,最后一个阶段就是根据这个新生成的 AST 生成新的代码。
Babel 是通过 generator 来生成新代码的:https://github.com/babel/babel/tree/master/packages/babel-generator,当然这个步骤也是深度优先遍历。
以上的三个阶段对应了三个 babel 的核心 package:
- babel-parser
- babel-traverse
- babel-generator
此外 Babel 还有几个较为核心的 package:
- babel-cli
- babel-polyfill
- babel-preset-env:预设,除此之外还有 flow,react,TypeScript
- babel-register
- babel-standalone:为浏览器和其他非node .js环境提供了一个独立的Babel构建。
工具包:
- babel-core
- babel-code-frame
- babel-runtime
- babel-template
- babel-types
如何实现一个 tiny-compiler?
https://github.com/jamiebuilds/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js
上述代码极具参考价值。
具体流程如下:
- tokenizer:将代码生成 token
- parser: 将 token 解析成 AST
- transformer:将 AST traverser 成 newAST
- generator:将 newAST 生成新代码
'use strict';
function tokenizer(input) {
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 });
continue;
}
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);
}
return tokens;
}
function parser(tokens) {
let current = 0;
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);
}
let ast = {
type: 'Program',
body: [],
};
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
function traverser(ast, visitor) {
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
function traverseNode(node, parent) {
let methods = visitor[node.type];
if (methods && methods.enter) {
methods.enter(node, parent);
}
switch (node.type) {
case 'Program':
traverseArray(node.body, node);
break;
case 'CallExpression':
traverseArray(node.params, node);
break;
case 'NumberLiteral':
case 'StringLiteral':
break;
default:
throw new TypeError(node.type);
}
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
traverseNode(ast, null);
}
function transformer(ast) {
let newAst = {
type: 'Program',
body: [],
};
ast._context = newAst.body;
traverser(ast, {
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
CallExpression: {
enter(node, parent) {
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
node._context = expression.arguments;
if (parent.type !== 'CallExpression') {
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
parent._context.push(expression);
},
}
});
return newAst;
}
function codeGenerator(node) {
switch (node.type) {
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';'
);
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
case 'Identifier':
return node.name;
case 'NumberLiteral':
return node.value;
case 'StringLiteral':
return '"' + node.value + '"';
default:
throw new TypeError(node.type);
}
}
function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
return output;
}
module.exports = {
tokenizer,
parser,
traverser,
transformer,
codeGenerator,
compiler,
};