什么是 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,};
