babel 是一个前端的代码转换工具,目的是为了让开发者使用ECMA最新的标准甚至一些在stage阶段的提案功能,而不用过多考虑运行环境的兼容性。javascript
近些年得益于js社区的活跃,ES版本从15年开始每一年都会发布一个新的版本,截止目前最新的版本是ECMAScript 2019,已是第10个版本了,可是目前最新的chrome还没有彻底支持ES6的全部功能,好比模块方面的功能,而babel的出现可让前端开发工程师们用最新的语法去coding,由它来保证在浏览器中代码的转换,换句话说它可让咱们用最新的标准写代码,而不用考虑浏览器的兼容~前端
若是想了解更多关于babel的内容,请移步这里,还有中文版文档java
上回分析的基础配置篇在这里node
babel 能够转化ES6的语法到ES5,以下git
// Babel 输入: ES2015 箭头函数
[1, 2, 3].map((n) => n + 1);
// Babel 输出: ES5 语法实现的同等功能
[1, 2, 3].map(function(n) {
return n + 1;
});
复制代码
全部被babel处理的js内容都会经历3个大的流程:github
本文实现的babel fork 自 the-super-tiny-compiler,内容基本同样,重在但愿能够对没有时间阅读代码的同窗起个快速理解的做用,后续若是有时间,会写一个和js相关的转换代码chrome
const code = '(add 2 (add 4 2))'; // 初始的code
const expectCode = 'add(2, add(4,2))'; // 指望转换结果
复制代码
const tokens = [
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' }
]
复制代码
const ast = {
"type": "Program",
"body": [
{
"type": "CallExpression",
"name": "add",
"params": [
{ "type": "NumericLiteral", "value": "2" },
{
"type": "CallExpression",
"name": "add",
"params": [{ "type": "NumericLiteral", "value": "4" }, { "type": "NumericLiteral", "value": "2" }]
}
]
}
]
}
复制代码
const newAst = {
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": { "type": "Identifier", "name": "add" },
"arguments": [
{ "type": "NumberLiteral", "value": "2" },
{
"type": "CallExpression",
"callee": { "type": "Identifier", "name": "add" },
"arguments": [
{ "type": "NumberLiteral", "value": "4" },
{ "type": "NumberLiteral", "value": "2" }
]
}
]
}
}
]
}
复制代码
const output = 'add(2, add(4, 2));'
复制代码
解析源代码,生成tokensexpress
const tokenizer = input => {
let current = 0; // 记录起始位置
const tokens = []; // 存放tokens
while (current < input.length) {
let char = input[current];
if (char === '(' || char === ')') {
// 遇到括号,转为type是paren的token
tokens.push({
type: 'paren',
value: char
});
current++; // 递增一位
continue;
}
if (/\s/.test(char)) {
// 空格无论,直接跳过 【空格在这里对咱们没有实际的意义,可是须要current下标跳过】
current++;
continue;
}
if (/[0-9]/.test(char)) {
// 若是是数字,用while来遍历到下一位不是数字的位置
let value = ''; // 用来放这个数字
while (/[0-9]/.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;
}
const LETTERS = /[a-z]/i; // 处理name
if (LETTERS.test(char)) {
// 处理连续的字母,变量的名称,也所以把type定义为name
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);
}
// 返回处理好的tokens
return tokens;
};
复制代码
解析第一步生成的tokens为对应的ast抽象语法树小程序
const parser = tokens => {
let current = 0;
// walk 函数 用来 parse token到对应的 【语法】 类型
function walk() {
let token = tokens[current];
if (token.type === 'number') {
current++;
return {
type: 'NumericLiteral',
value: token.value
};
}
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value
};
}
// 遇到括号的时候,咱们认为开始进行方法调用的转换了
if (token.type === 'paren' && token.value === '(') {
token = tokens[++current]; // 跳过 '(' 自己这个token,它只是个标记,在这里没有实际做用
let node = {
// 定义一个node节点,类型就是 CallExpression
type: 'CallExpression',
name: token.value, // 咱们认为括号接下来的token 放的就是这个方法的 name
params: [] // 默认方法的参数是一个空数组
};
token = tokens[++current]; // 跳过name这个token
// 遍历name以后的token,必定要遇到方法调用的结束符号 '(' 为止
while (token.type !== 'paren' || (token.type === 'paren' && token.value !== ')')) {
node.params.push(walk()); // 在'('和')' 中间全部的token,咱们都认为是当前方法调用时候的参数,所以递归解析就能够了~
token = tokens[current]; // 这里每次从新获取一下当前的token,为了抛出错误的时候用到
}
current++; // 和前面同样,处理完当前current 加一位到下一个
return node; // 返回当前构建好的node
}
throw new TypeError(token.type);
}
// 定义原始的ast结构
const ast = {
type: 'Program', // 入口开始咱们称为 Program
body: []
};
// 遍历tokens
while (current < tokens.length) {
ast.body.push(walk()); // 递归tokens,填充ast
}
return ast; // 返回填充结束的ast~
};
复制代码
根据原始ast转化为新的ast数组
// 拿到ast后,就能够进行下一步的“转化”流程了,可是在这以前咱们须要先设计一个traverser方法,这个方法会对生成好的ast进行深度优先的递归遍历,同时还能够提供一个visitor参数用来“访问”每个语法类型的遍历过程,有了visitor就能够在“访问”到当前type类型的node的时候,对当前的node进行一些额外的处理了,这也是babel插件的工做原理,其实仍是在对ast的转化过程当中进行的处理~
const traverser = (ast, visitor) => {
// traverseArray 会遍历array,对每个item进行traverseNode转化
function traverseArray(array, parent) {
array.forEach(el => {
traverseNode(el, parent);
});
}
// 转化过程,接受2个参数 当前转化的node 和 父节点parent
function traverseNode(node, parent) {
// 先看下提供的visitor中有没有当前node.type类型的method
// 从babel的官网中能够看到method能够直接经过 [type](){}这种function的类型进行定义,也能够经过一个config,包含enter 和 exit的形式来定义~
// 树的遍历过程当中,咱们对每个节点会有2次访问的机会,第一次是进入,第二次是退出,对应这里的 enter 和 exit
const method = visitor[node.type];
if (typeof method === 'function') {
// 若是method是方法,直接调用,并送入node和parent
method(node, parent);
} else if (method && method.enter) {
// 若是是enter,则调用enter~
method.enter(node, parent);
}
switch (
node.type // 对每一个type进行转化
) {
case 'Program':
traverseArray(node.body, node); // 若是是Program,咱们知道他下面的子节点都在body中,所以咱们经过traverseArray把子节点的数组和当前节点送进去
break;
case 'CallExpression':
traverseArray(node.params, node); // 若是是CallExpression,咱们知道CallExpression的子节点声名在params这个数组中,一样的对子节点进行递归转化
break;
case 'NumericLiteral': // 当遇到 NumericLiteral 和 StringLiteral 的时候,它们都没有子节点了,所以直接break就能够了
case 'StringLiteral':
break;
default:
throw new TypeError(node.type); // 一样,找不到就丢出去一个错误
}
if (typeof method === 'function') {
// 每一个节点在遍历结束后,咱们须要调用visitor的exit方法,相似于enter的调用
method(node, parent);
} else if (method && method.exit) {
method.exit(node, parent);
}
}
traverseNode(ast, null); // 递归的启动,咱们的第一个节点就是ast,它是没有父节点的,所以第二个参数给null~
};
// 接下来是对ast进行默认的转化的过程,参数就是ast
const transformer = ast => {
const newAst = {
// 转化后的结果会保存到一个新的newAst中去
type: 'Program',
body: []
};
// 这里是一个hack,真正的实现比这个要复杂,咱们为了实例简单,经过_context来传递新老ast之间的上下文的对应关系
// 由于虽然咱们遍历的是旧的ast,可是须要转化的结果保存到新的ast中,所以存在一个节点之间的对应关系
// 咱们给旧的ast加一个_context属性,而且让它指向实际的新的ast的body
// 这里指向body是由于最外层的type类型是Program 而且他们子节点都是body,所以我只要后面修改parent的_context,就会更新到新的ast的body中
ast._context = newAst.body;
traverser(ast, {
// 利用 traverser 开始遍历
NumericLiteral: {
enter(node, parent) {
// 遇到 NumericLiteral 类型,转化为新的带有语法意义的node,并经过 parent._context push 到新的ast对应的节点中
// 请注意这里的用词 parent._context 是ast对应的新节点,不会一直是第一层的body节点
// ----- 分割线 ----- 第一次看到这里请继续往下看代码,忽略掉下面紧挨着的【第一次先忽略】的注释
// [第一次先忽略]
// ok,这里咱们继续遍历到 NumericLiteral 的时候,可能已经遍历到了 CallExpression 下的子节点 params 中,可是这个时候,请注意:
// parent._context 指向的是 expression.arguments 这个节点哦,这就解释了这里是如何经过 parent._context hack了新老ast的对应关系的指向
parent._context.push({
type: 'NumberLiteral',
value: node.value
});
}
},
StringLiteral: {
// StringLiteral 同上
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value
});
}
},
CallExpression: {
// 这里要注意,遇到方法调用表达式的时候,咱们要转为新的表达式类型
enter(node, parent) {
let expression = {
// 定义新的调用方法的语法node格式
type: 'CallExpression',
callee: {
// 存放方法名称
type: 'Identifier',
name: node.name
},
arguments: [] // 存放调用参数
};
// 由于 Program 类型下的子节点都在body中,咱们用 _context 来指向新的body,这里的原理是同样的
// 类型 CallExpression 的全部子节点在 expression.arguments 所以咱们把当前node的 _context 指向 expression.arguments
// 请回到上面的 【第一次先忽略】 节点所在的注释部分
node._context = expression.arguments;
if (parent.type !== 'CallExpression') {
// 当父节点的类型不在是 CallExpression 的时候,咱们知道这个表达式部分已经结束了
expression = {
type: 'ExpressionStatement',
expression: expression
};
}
parent._context.push(expression); // 把结果经过 parent._context 保存到新ast的对应节点中去
}
}
});
return newAst; // 递归结束后 返回新的ast
};
复制代码
根据新的ast,生成咱们处理事后的代码
const codeGenerator = node => {
// 区分不一样的类型,进行不一样的 generator
switch (node.type) {
case 'Program':
return node.body.map(codeGenerator).join('\n'); // 对于 Program 只须要加上换行符
case 'ExpressionStatement':
return codeGenerator(node.expression) + ';'; // 对于表达式,递归表达式的expression,而后加上分号 保证代码容许的正确性
case 'CallExpression':
// 对于方法调用要注意下,咱们的初始的name token被放到callee属性中,同时 params被放到arguments中,所以须要递归这2个属性
// 同时对于 arguments 要先加括号,再用 , 隔开每一项 以便于生成(x,y,z)这样的调用格式
return codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')';
case 'Identifier':
return node.name; // 对于变量直接返回name就能够
case 'NumberLiteral':
return node.value; // 对于数字返回value
case 'StringLiteral':
return '"' + node.value + '"'; // 对于字符串须要加上双引号,切记咱们在转码,不是在运行~
default:
throw new Error(node.type); // 没法解析的 抛出错误
}
};
复制代码
到此,咱们的迷你版babel就实现好了,来测试一下:
const code = '(add 2 (add 4 2))'; // 初始的code
const ast = parser(tokenizer(code));
const newAst = transformer(ast);
const output = codeGenerator(newAst);
console.log(output); // 'add(2, add(4,2))'
复制代码
完美,真正的babel源码中会处理不少细节的问题,可是从原理来说,你们都是相通的~,有兴趣的话能够去翻翻babel的源码~
若是对你有点帮助,但愿顺手给个赞,您的赞是我坚持输出的动力,谢谢~