1. 什么是 Babel
简略地说,Babel 可能转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也可能运行。
// es2015 的 const 和 arrow function
const add = (a, b) => a + b;
// Babel 转译后
var add = function add(a, b) {return a + b;};
Babel 的性能很纯正。咱们传递一段源代码给 Babel,而后它返回一串新的代码给咱们。就是这么简略,它不会运行咱们的代码,也不会去打包咱们的代码。
它只是一个编译器。
赫赫有名的 Taro 也是利用 Babel 将 React 语法转化成小程序模板。
2. Babel 的包形成
外围包
- babel-core:babel 转译器自身,提供了 babel 的转译 API,如 babel.transform 等,用于对代码进行转译。像 webpack 的 babel-loader 就是调用这些 API 来实现转译过程的。
- babylon:js 的词法解析器,AST 生成
- babel-traverse:用于对 AST(形象语法树,想理解的请自行查问编译原理)的遍历,次要给 plugin 用
- babel-generator:依据 AST 生成代码
性能包
- babel-types:用于测验、构建和扭转 AST 树的节点
- babel-template:辅助函数,用于从字符串模式的代码来构建 AST 树节点
- babel-helpers:一系列预制的 babel-template 函数,用于提供给一些 plugins 应用
- babel-code-frames:用于生成错误信息,打印出谬误点源代码帧以及指出出错地位
- babel-plugin-xxx:babel 转译过程中应用到的插件,其中 babel-plugin-transform-xxx 是 transform 步骤应用的
- babel-preset-xxx:transform 阶段应用到的一系列的 plugin(官网写好的插件)
- babel-polyfill:JS 规范新增的原生对象和 API 的 shim,实现上仅仅是 core-js 和 regenerator-runtime 两个包的封装
- babel-runtime:性能相似 babel-polyfill,个别用于 library 或 plugin 中,因为它不会净化全局作用域
工具包
babel-cli:babel 的命令行工具,通过命令行对 js 代码进行转译
babel-register:通过绑定 node.js 的 require 来主动转译 require 援用的 js 代码文件
babel8 将包名变为了 @babel
3. 原理
Babel 转换 JS 代码能够分成以下三个大步骤:
- Parser(解析):此过程承受转换之前的源码,输入 AST(形象语法树)。在 Babel 中负责此过程的包为 babel/parser;
- Transform(转换):此过程承受 Parser 输入的 AST(形象语法树),输入转换后的 AST(形象语法树)。在 Babel 中负责此过程的包为 @babel/traverse;
- Generator(生成):此过程承受 Transform 输入的新 AST,输入转换后的源码。在 Babel 中负责此过程的包为 @babel/generator。
所以 AST 相干常识,你应该事后就理解了
babel 是一个转译器,感觉绝对于编译器 compiler,叫转译器 transpiler 更精确,因为它只是把同种语言的高版本规定翻译成低版本规定,而不像编译器那样,输入的是另一种更低级的语言代码。
然而和编译器相似,babel 的转译过程也分为三个阶段:parsing、transforming、generating,以 ES6 代码转译为 ES5 代码为例,babel 转译的具体过程如下:
(1)code –> AST
第一步就是把咱们写的 ES6 代码字符串转换成 ES6 AST
那转换的工具为 babel 的 parser
怎么转换的你就了解为失常的转 AST,简略的例子会放到结尾
(2)Transform
这一步做的事件,就是 操作 AST。将 ES6 的 AST 操作 JS 转换成 ES5 的 AST
Transform 会遍历 AST,在此过程中会对 AST 构造进行增加、移除、更新等操作,当然这些操作依赖开发者提供的插件。Babel 对每一个 AST 节点都提供了「进入节点 enter」与「退出节点 exit」两个机会,第三方开发者能够利用这两个机会对旧 AST 做操作。值得一提的是,Transform 步骤是 Babel 最简单的局部,也是 第三方插件能大显神通的中央。
这一步是最重要的中央,相似 webpack,插件 plugins 就是在这里失效,也能够本人手写插件退出其中。
Transform 过程采纳的是典型的 访问者模式 不相熟的同学能够理解一下。
咱们能够看到 AST 中有很多类似的元素,它们都有一个 type 属性,这样的元素被称作节点。一个节点通常含有若干属性,能够用于形容 AST 的局部信息。
比方这是一个最常见的 Identifier 节点:
{
type: 'Identifier',
name: 'add'
}
示意这是一个标识符。
所以,操作 AST 也就是操作其中的节点,能够增删改这些节点,从而转换成理论须要的 AST。
Babel 对于 AST 的遍历是深度优先遍历,对于 AST 上的每一个分支 Babel 都会先向下遍历走到止境,而后再向上遍历退出刚遍历过的节点,而后寻找下一个分支。
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration", // 变量申明
"declarations": [ // 具体申明
{
"type": "VariableDeclarator", // 变量申明
"id": {
"type": "Identifier", // 标识符(最根底的)"name": "add" // 函数名
},
"init": {
"type": "ArrowFunctionExpression", // 箭头函数
"id": null,
"expression": true,
"generator": false,
"params": [ // 参数
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
],
"body": { // 函数体
"type": "BinaryExpression", // 二项式
"left": { // 二项式右边
"type": "Identifier",
"name": "a"
},
"operator": "+", // 二项式运算符
"right": { // 二项式左边
"type": "Identifier",
"name": "b"
}
}
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
根节点咱们就不说了,从 declarations 里开始遍历:
- 申明了一个变量,并且晓得了它的外部属性(id、init),而后咱们再以此拜访每一个属性以及它们的子节点。
- id 是一个 Idenrifier,有一个 name 属性示意变量名。
- 之后是 init,init 也有好几个外部属性:
- type 是 ArrowFunctionExpression,示意这是一个箭头函数表达式
- • params 是这个箭头函数的入参,其中每一个参数都是一个 Identifier 类型的节点;
- • body 属性是这个箭头函数的主体,这是一个 BinaryExpression 二项式:left、operator、right,别离示意二项式的右边变量、运算符以及左边变量。
这是遍历 AST 的文言模式,再看看 Babel 是怎么做的:
Babel 会保护一个称作 Visitor 的对象,这个对象定义了用于 AST 中获取具体节点的办法。
Visitor
Babel 遍历 AST 其实会通过两次节点:遍历的时候和退出的时候,所以实际上 Babel 中的 Visitor 应该是这样的:
var visitor = {
Identifier: {enter() {console.log('Identifier enter');
},
exit() {console.log('Identifier exit');
}
}
};
比方咱们拿这个 visitor 来遍历这样一个 AST:
params: [ // 参数
{
"type": "Identifier",
"name": "a"
},
{
"type": "Identifier",
"name": "b"
}
]
过程可能是这样的 …
- 进入 Identifier(params[0])
- 走到止境
- 退出 Identifier(params[0])
- 进入 Identifier(params[1])
- 走到止境
- 退出 Identifier(params[1])
当然,Babel 中的 Visitor 模式远远比这简单 …
回到下面的,箭头函数是 ES5 不反对的语法,所以 Babel 得把它转换成一般函数,一层层遍历上来,找到了 ArrowFunctionExpression 节点,这时候就须要把它替换成 FunctionDeclaration 节点。所以,箭头函数可能是这样解决的:
import * as t from "@babel/types";
var visitor = {ArrowFunction(path) {path.replaceWith(t.FunctionDeclaration(id, params, body));
}
};
(3) Generate(代码生成)
上一步是将 ES6 的 AST 操作 JS 转换成 ES5 的 AST
这一步就是将 ES5 的 AST 转换成 ES5 代码字符串
通过下面两个阶段,须要转译的代码曾经通过转换,生成新的 AST 了,最初一个阶段理所应当就是依据这个 AST 来输入代码。
Babel 是深度优先遍历。
Generator 能够看成 Parser 的逆向操作,依据新的 AST 生成代码,其实就是生成字符串,这些字符串自身没有意义,是编译器赋予了字符串意义才变成咱们所说的「代码」。Babel 会深度优先遍历整个 AST,而后构建能够示意转换后代码的字符串。
class Generator extends Printer {constructor(ast, opts = {}, code) {const format = normalizeOptions(code, opts);
const map = opts.sourceMaps ? new SourceMap(opts, code) : null;
super(format, map);
this.ast = ast;
}
ast: Object;
generate() {return super.generate(this.ast);
}
}
通过这三个阶段,代码就被 Babel 转译胜利了。参考:前端进阶面试题具体解答
4. 简略实现
以 const add = (a, b) => a + b 为例,转化实现后应该变成 function add(a,b) {return a + b}。
定义待转化的代码字符串:
/** * 待转化的代码 */
const codeString = 'const add = (a, b) => a + b';
(1)ES6 code –> AST
生成 AST 是须要进行字符串词法剖析和语法分析的
首先进行词法剖析
/**
* Parser 过程 - 词法剖析
* @param codeString 待转化的字符串
* @returns Tokens 令牌流
*/
function tokens(codeString) {let tokens = []; // 寄存 token 的数组
let current = 0; // 以后的索引
while (current < codeString.length) {let char = codeString[current];
// 先解决括号
if (char === '(' || char === ')') {
tokens.push({
type: 'parens',
value: char
});
current++;
continue;
}
// 解决空格,空格可能是多个间断的,所以须要将这些间断的空格一起放到 token 数组中
const WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
let value = '';
while (current < codeString.length && WHITESPACE.test(char)) {
value = value + char;
current++;
char = codeString[current];
}
tokens.push({
type: 'whitespace',
value: value
});
continue;
}
// 解决间断数字, 数字也可能是间断的,原理同上
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
let value = '';
while (current < codeString.length && NUMBERS.test(char)) {
value = value + char;
current++;
char = codeString[current];
}
tokens.push({
type: 'number',
value: value
});
continue;
}
// 解决标识符,标识符个别以字母、_、$ 结尾的间断字符
const LETTERS = /[a-zA-Z\$\_]/;
if (LETTERS.test(char)) {
let value = '';
// 标识符
while (current < codeString.length && /[a-zA-Z0-9\$\_]/.test(char)) {
value = value + char;
current++;
char = codeString[current];
}
tokens.push({
type: 'identifier',
value: value
});
continue;
}
// 解决 , 分隔符
const COMMA = /,/;
if (COMMA.test(char)) {
tokens.push({
type: ',',
value: ','
});
current++;
continue;
}
// 解决运算符
const OPERATOR = /=|\+|>/;
if (OPERATOR.test(char)) {
let value = '';
while (OPERATOR.test(char)) {
value += char;
current++;
char = codeString[current];
}
// 如果存在 => 则阐明遇到了箭头函数
if (value === '=>') {
tokens.push({
type: 'ArrowFunctionExpression',
value,
});
continue;
}
tokens.push({
type: 'operator',
value
});
continue;
}
throw new TypeError(` 还未退出此字符解决 ${char}`);
}
return tokens;
}
语法分析
/** * Parser 过程 - 语法分析 * @param tokens 令牌流 * @returns AST */
const parser = tokens => {
// 申明一个全时指针,它会始终存在
let current = -1;
// 申明一个暂存栈, 用于寄存长期指针
const tem = [];
// 指针指向的以后 token
let token = tokens[current];
const parseDeclarations = () => {
// 暂存以后指针
setTem();
// 指针后移
next();
// 如果字符为 'const' 可见是一个申明
if (token.type === 'identifier' && token.value === 'const') {
const declarations = {
type: 'VariableDeclaration',
kind: token.value
};
next();
// const 前面要跟变量的, 如果不是则报错
if (token.type !== 'identifier') {throw new Error('Expected Variable after const');
}
// 咱们获取到了变量名称
declarations.identifierName = token.value;
next();
// 如果跟着 '=' 那么前面应该是个表达式或者常量之类的, 这里咱们只反对解析函数
if (token.type === 'operator' && token.value === '=') {declarations.init = parseFunctionExpression();
}
return declarations;
}
};
const parseFunctionExpression = () => {next();
let init;
// 如果 '=' 前面跟着括号或者字符那根本判断是一个表达式
if ((token.type === 'parens' && token.value === '(') ||
token.type === 'identifier'
) {setTem();
next();
while (token.type === 'identifier' || token.type === ',') {next();
}
// 如果括号后跟着箭头, 那么判断是箭头函数表达式
if (token.type === 'parens' && token.value === ')') {next();
if (token.type === 'ArrowFunctionExpression') {
init = {
type: 'ArrowFunctionExpression',
params: [],
body: {}};
backTem();
// 解析箭头函数的参数
init.params = parseParams();
// 解析箭头函数的函数主体
init.body = parseExpression();} else {backTem();
}
}
}
return init;
};
const parseParams = () => {const params = [];
if (token.type === 'parens' && token.value === '(') {next();
while (token.type !== 'parens' && token.value !== ')') {if (token.type === 'identifier') {
params.push({
type: token.type,
identifierName: token.value
});
}
next();}
}
return params;
};
const parseExpression = () => {next();
let body;
while (token.type === 'ArrowFunctionExpression') {next();
}
// 如果以(结尾或者变量结尾阐明不是 BlockStatement, 咱们以二元表达式来解析
if (token.type === 'identifier') {
body = {
type: 'BinaryExpression',
left: {
type: 'identifier',
identifierName: token.value
},
operator: '',
right: {
type: '',
identifierName: ''
}
};
next();
if (token.type === 'operator') {body.operator = token.value;}
next();
if (token.type === 'identifier') {
body.right = {
type: 'identifier',
identifierName: token.value
};
}
}
return body;
};
// 指针后移的函数
const next = () => {
do {
++current;
token = tokens[current]
? tokens[current]
: {type: 'eof', value: ''};
} while (token.type === 'whitespace');
};
// 指针暂存的函数
const setTem = () => {tem.push(current);
};
// 指针回退的函数
const backTem = () => {current = tem.pop();
token = tokens[current];
};
const ast = {
type: 'Program',
body: []};
while (current < tokens.length) {const statement = parseDeclarations();
if (!statement) {break;}
ast.body.push(statement);
}
return ast;
};
能够大略认为,转成 AST 的过程中就是一直的循环、正则、标识符比对等一系列的操作
(2)Transform
const traverser = (ast, visitor) => {
// 如果节点是数组那么遍历数组
const traverseArray = (array, parent) => {array.forEach((child) => {traverseNode(child, parent);
});
};
// 遍历 ast 节点
const traverseNode = (node, parent) => {const methods = visitor[node.type];
if (methods && methods.enter) {methods.enter(node, parent);
}
switch (node.type) {
case 'Program':
traverseArray(node.body, node);
break;
case 'VariableDeclaration':
traverseArray(node.init.params, node.init);
break;
case 'identifier':
break;
default:
throw new TypeError(node.type);
}
if (methods && methods.exit) {methods.exit(node, parent);
}
};
traverseNode(ast, null);
};
/**
* Transform 过程
* @param ast 待转化的 AST
* 此函数会调用 traverser,传入自定义的 visitor 实现 AST 转化
*/
const transformer = (ast) => {
// 新 ast
const newAst = {
type: 'Program',
body: []};
// 此处在 ast 上新增一个 _context 属性,与 newAst.body 指向同一个内存地址,traverser 函数操作的 ast_context 都会赋值给 newAst.body
ast._context = newAst.body;
traverser(ast, {
VariableDeclaration: {enter(node, parent) {
let functionDeclaration = {params: []
};
if (node.init.type === 'ArrowFunctionExpression') {
functionDeclaration.type = 'FunctionDeclaration';
functionDeclaration.identifierName = node.identifierName;
functionDeclaration.params = node.init.params;
}
if (node.init.body.type === 'BinaryExpression') {
functionDeclaration.body = {
type: 'BlockStatement',
body: [{type: 'ReturnStatement', argument: node.init.body}],
};
}
parent._context.push(functionDeclaration);
}
},
});
return newAst;
};
(3) generate
/** * Generator 过程 * @param node 新的 ast * @returns 新的代码 */
const generator = (node) => {switch (node.type) {
// 如果是 `Program` 结点,那么咱们会遍历它的 `body` 属性中的每一个结点,并且递归地
// 对这些结点再次调用 codeGenerator,再把后果打印进入新的一行中。case 'Program':
return node.body.map(generator)
.join('\n');
// 如果是 FunctionDeclaration 咱们别离遍历调用其参数数组以及调用其 body 的属性
case 'FunctionDeclaration':
return 'function' + '' + node.identifierName +'('+ node.params.map(generator) +')'+' ' + generator(node.body);
// 对于 `Identifiers` 咱们只是返回 `node` 的 identifierName
case 'identifier':
return node.identifierName;
// 如果是 BlockStatement 咱们遍历调用其 body 数组
case 'BlockStatement':
return '{' + node.body.map(generator) + '}';
// 如果是 ReturnStatement 咱们调用其 argument 的属性
case 'ReturnStatement':
return 'return' + ' ' + generator(node.argument);
// 如果是 ReturnStatement 咱们调用其左右节点并拼接
case 'BinaryExpression':
return generator(node.left) + '' + node.operator +' ' + generator(node.right);
// 没有合乎的则报错
default:
throw new TypeError(node.type);
}
};
(4) 整个流程串联起来,实现调用链
let token = tokens(codeString);
let ast = parser(token);
let newAST = transformer(ast);
let newCode = generator(newAST);
console.log(newCode);
5. 其余扩大常识
此外,还要留神很重要的一点就是,babel 只是转译新规范引入的语法,比方 ES6 的箭头函数转译成 ES5 的函数;而新规范引入的新的原生对象,局部原生对象新增的原型办法,新增的 API 等(如 Proxy、Set 等),这些 babel 是不会转译的。须要用户自行引入 polyfill 来解决
plugins
插件利用于 babel 的转译过程,尤其是第二个阶段 transforming,如果这个阶段不应用任何插件,那么 babel 会原样输入代码。
咱们次要关注 transforming 阶段应用的插件,因为 transform 插件会主动应用对应的词法插件,所以 parsing 阶段的插件不须要配置。
presets
如果要自行配置转译过程中应用的各类插件,那太苦楚了,所以 babel 官网帮咱们做了一些预设的插件集,称之为 preset,这样咱们只须要应用对应的 preset 就能够了。以 JS 规范为例,babel 提供了如下的一些 preset:
• es2015
• es2016
• es2017
• env
es20xx 的 preset 只转译该年份批准的规范,而 env 则代指最新的规范,包含了 latest 和 es20xx 各年份
另外,还有 stage- 0 到 stage- 4 的规范成形之前的各个阶段,这些都是试验版的 preset,倡议不要应用。
polyfill
polyfill 是一个针对 ES2015+ 环境的 shim,实现上来说 babel-polyfill 包只是简略的把 core-js 和 regenerator runtime 包装了下,这两个包才是真正的实现代码所在(后文会具体介绍 core-js)。
应用 babel-polyfill 会把 ES2015+ 环境整体引入到你的代码环境中,让你的代码能够间接应用新规范所引入的新原生对象,新 API 等,一般来说独自的利用和页面都能够这样应用。
runtime
polyfill 和 runtime 的区别(必看)
间接应用 babel-polyfill 对于利用或页面等环境在你管制之中的状况来说,并没有什么问题。然而对于在 library 中应用 polyfill,就变得不可行了。因为 library 是供内部应用的,但内部的环境并不在 library 的可控范畴,而 polyfill 是会净化原来的全局环境的(因为新的原生对象、API 这些都间接由 polyfill 引入到全局环境)。这样就很容易会发生冲突,所以这个时候,babel-runtime 就能够派上用场了。