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的源碼~
若是對你有點幫助,但願順手給個贊,您的贊是我堅持輸出的動力,謝謝~