看 babel doc 的時(shí)候,首頁看到了一個(gè)鏈接,說是能從 high level 解釋 babel 是怎么工作的 !!!
以下是babel官網(wǎng)提供的鏈接中的大致內(nèi)容,有興趣的戳這里看原文
簡單說明
編譯 是從 源代碼(通常為高級語言) 到能直接被計(jì)算機(jī)編譯器或虛擬機(jī)執(zhí)行的 目標(biāo)代碼(通常為低級語言或機(jī)器語言) 的翻譯過程.
如何用 JS 寫一個(gè)超級小巧 (200行代碼預(yù)定) 的編譯器.
我們即將 (通過這個(gè)編譯器) 把一些看著像 lisp 的代碼, 編譯成類 C 代碼.
其實(shí)和把 ES6 轉(zhuǎn)為 ES5, JSX/TS 轉(zhuǎn)為 JS 一個(gè)套路.
具體的例子就是這個(gè)樣子
LISP C
2 + 2 (add 2 2) add(2, 2)
4 - 2 (subtract 4 2) subtract(4 2)
2 + (4 - 2) (add 2 (subtract 4 2)) add(2, subtract(4, 2))
非常簡單吧?
雖然這既不是 LISP 語法也不是 C 語法. 但足夠展示現(xiàn)代編譯器的主要部分了.
背景
大多數(shù)編譯器分為三個(gè)主要階段:解析皆看,轉(zhuǎn)換,代碼生成.
- 解析 : 將 原始代碼 轉(zhuǎn)換為 更抽象的表示背零。
- 轉(zhuǎn)換 : 將這個(gè) 抽象的表示 轉(zhuǎn)化為編譯器想要的東西.
- 代碼生成 : 將轉(zhuǎn)化后的表示 轉(zhuǎn)化為新的代碼.
解析
解析通常分為兩個(gè)階段:詞法分析 和 句法分析
1 詞法分析 (Lexical Analysis)
獲取 原始代碼 并通過 標(biāo)記化器tokenizer(詞法分析器lexer) 將其 拆分為 tokens
tokens 是一組非常小的 object腰吟,描述語法中的一個(gè)獨(dú)立的部分。它們可能是數(shù)字徙瓶,標(biāo)簽毛雇,標(biāo)點(diǎn)符號嫉称,運(yùn)算符, 等等等等。
2 語法分析 (Syntactic Analysis)
獲取 token 并將其重新格式化為 描述語法的各個(gè)部分及其關(guān)系 的一種表現(xiàn)形式灵疮。這種表現(xiàn)形式稱為 中間語言(intermediate representation) , 也叫 抽象語法樹织阅。
比如:
(add 2 (subtract 4 2))
這其中的 tokens 大概會(huì)是:
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' },
]
抽象語法樹 Abstract Syntax Tree (AST) 大概長這樣:
{
type: 'Program',
body: [{
type: 'CallExpression',
name: 'add',
params: [{
type: 'NumberLiteral',
value: '2',
}, {
type: 'CallExpression',
name: 'subtract',
params: [{
type: 'NumberLiteral',
value: '4',
}, {
type: 'NumberLiteral',
value: '2',
}]
}]
}]
}
這樣看起不爽, 其實(shí) body 中的內(nèi)容就是
add
/ \
2 substract
/ \
4 2
轉(zhuǎn)換
將 AST 轉(zhuǎn)化為同樣的語言, 或者其他完全不同的語言.
看看怎么轉(zhuǎn)化 AST 吧.
你可能注意到了, 我們的 AST 中有一些元素, 看起來都差不多.
都有一個(gè) type 屬性. 這些我們把它叫做 AST 節(jié)點(diǎn).
這些節(jié)點(diǎn)定義了自身的屬性.
比如說這是一個(gè)數(shù)字:
{
type: 'NumberLiteral',
value: '2',
}
這是一個(gè)調(diào)用表達(dá)式:
{
type: 'CallExpression',
name: 'add',
params: [ ...一些內(nèi)嵌節(jié)點(diǎn)... ]
}
轉(zhuǎn)換就是對 AST 節(jié)點(diǎn)的操作, 我們增加,刪除,替換里面的屬性.
也可以增加新的節(jié)點(diǎn), 刪除節(jié)點(diǎn).
我們也可以把這個(gè) AST 扔在這不操作, 而是基于它建一個(gè)新的出來.
因?yàn)槲覀冞@里, 要把代碼轉(zhuǎn)換為另一種語言(LIST to C), 我們后面就建一個(gè)新的 AST 好了.
遍歷
為了瀏覽節(jié)點(diǎn),我們需要能夠遍歷他們震捣。
此遍歷過程將使用 深度優(yōu)先 來訪問 AST 中的每個(gè)節(jié)點(diǎn).
對于這個(gè) AST.
Program
|
add
/ \
2 substract
/ \
4 2
我們遍歷的順序是:
- Program
- CallExpression (add)
- NumberLiteral (2)
- CallExpression (substract)
- NumberLiteral (4)
- NumberLiteral (2)
如果我們要直接操作這棵樹的話, 介紹一下不同的遍歷方式還是有用的,
然而我們只是建一顆新的, 所以深度優(yōu)先遍歷足夠了.
訪客 vistor
基本思路是: 我們創(chuàng)建一個(gè)訪客 vistor 對象, 它有不同方法能接受不同類型 (type) 的節(jié)點(diǎn).
var visitor = {
NumberLiteral() {},
CallExpression() {},
};
遍歷過程中, 當(dāng)我們進(jìn)入 (enter) AST 中每一個(gè)節(jié)點(diǎn)的時(shí)候, 我們都會(huì)根據(jù)節(jié)點(diǎn) type 調(diào)用這個(gè) vistor 的對應(yīng)方法.
為了弄清楚類型, 這個(gè)函數(shù)需要接收當(dāng)前節(jié)點(diǎn) node
為了遍歷, 這個(gè)函數(shù)需要知道它的父節(jié)點(diǎn) parent
var visitor = {
NumberLiteral(node, parent) {},
CallExpression(node, parent) {},
};
不過, 我們可能還需要考慮一下 "退出 exit"
看一下樹:
- Program
- CallExpression
- NumberLiteral
- CallExpression
- NumberLiteral
- NumberLiteral
當(dāng)我們遍歷時(shí), 可能走到?jīng)]有子節(jié)點(diǎn)的分支, 我們就該 exit 了,
所以向下遍歷我們叫做 "enter", 往回退我們叫做 "exit".
-> Program (enter)
-> CallExpression (enter)
-> Number Literal (enter)
<- Number Literal (exit)
-> Call Expression (enter)
-> Number Literal (enter)
<- Number Literal (exit)
-> Number Literal (enter)
<- Number Literal (exit)
<- CallExpression (exit)
<- CallExpression (exit)
<- Program (exit)
為了支持 enter 和 exit, 最終我們的 vistor 差不多是這樣:
var visitor = {
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
}
};
代碼生成
最后一步啦.
有一些 compiler 可能會(huì)把 transform 和這一步疊加在一起,
但是 代碼生成(code generation) 這一步的主要目的是, 把我們的AST轉(zhuǎn)為代碼字符串(string-ify)蒲稳。
代碼生成有很多種做法, 大多數(shù)就是用我們剛才生成的 AST 來做的.
我們的 code generator 應(yīng)該知道怎么打印出這些不同類型的 node (遞歸 nodes 是常見操作)
ta-da!
完畢, 這就是編譯器的所有不同部分.
并不是說每個(gè)編譯器看起來都像我在這里描述的那樣.編譯器有許多不同的用途,它們可能需要更多的步驟.
你現(xiàn)在應(yīng)該大概知道它是怎么運(yùn)作的.
代碼時(shí)間
點(diǎn)這里 獲取代碼
(/ ^ ▽ ^ )/ tokenizer
我們將從解析第一階段,詞法分析 tokenizer (token 化)開始.
也就是把代碼字符串, 轉(zhuǎn)化為一組 token
(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]
function tokenizer(input) {
// current 標(biāo)記我們現(xiàn)在在 code 的哪個(gè)位置, 其實(shí)就是 index
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;
}
// 數(shù)字
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
let value = '';
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'number', value });s
continue;
}
// 也處理一下 string 字符串
if (char === '"') {
let value = '';
char = input[++current];
while (char !== '"') {
value += char;
char = input[++current];
}
char = input[++current];
tokens.push({ type: 'string', value });
continue;
}
// 處理函數(shù)名
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);
}
}
ヽ/?o ?? o\? parser
接收一個(gè) token 數(shù)組, 返回一個(gè) AST
[{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }
// 接收一個(gè) token 數(shù)組
function parser(tokens) {
let current = 0;
// 遞歸 recursion
function walk() {
let token = tokens[current];
if (token.type === 'number') {
current++;
// 數(shù)字節(jié)點(diǎn), 直接返回
return {
type: 'NumberLiteral',
value: token.value,
};
}
if (token.type === 'string') {
current++;
// 字符串節(jié)點(diǎn), 直接返回
return {
type: 'StringLiteral',
value: token.value,
};
}
// 遇到一個(gè)左括號, 調(diào)用表達(dá)式
if (
token.type === 'paren' &&
token.value === '('
) {
// 創(chuàng)建一個(gè)調(diào)用節(jié)點(diǎn)
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
token = tokens[++current];
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
// 遇到右括號之前, 在這個(gè)節(jié)點(diǎn)下遍歷, 都放入調(diào)用參數(shù)中.
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
// AST 根節(jié)點(diǎn)
let ast = {
type: 'Program',
body: [],
};
while (current < tokens.length) {
// 遍歷所有節(jié)點(diǎn), 添加進(jìn)去
ast.body.push(walk());
}
return ast;
}
the token array
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '4' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' }, //<<< Closing parenthesis
{ type: 'paren', value: ')' }, //<<< Closing parenthesis
]
the tree
(
/
param
/ | \
"add" 2 (
|
param
/ | \
"sub" 4 2
AST DONE!!
⌒(?>???<?)⌒ traverser
使用 visitor 訪問不同 AST 節(jié)點(diǎn), 在遇到不同 type 的節(jié)點(diǎn) call 不同的方法.
函數(shù)結(jié)構(gòu):
traverse(ast, {
Program: {
enter(node, parent) {},
exit(node, parent) {}
},
CallExpression: {
enter(node, parent) {},
exit(node, parent) {},
},
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
},
})
定義一個(gè) traverser 方法接收一個(gè) AST 和 visitor. 內(nèi)部有兩個(gè) functions
// 定義一個(gè) traverser 方法接收一個(gè) AST 和 visitor. 內(nèi)部有兩個(gè) functions
function traverser(ast, visitor) {
// `traverseArray` 可以讓我們遍歷數(shù)組, 并調(diào)用我們定義的另一個(gè)方法 `traverseNode`
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// `traverseNode` 接收 `node` 和它的 `parent` 節(jié)點(diǎn).
// 這樣我們就能傳 visitor 的兩個(gè) methods.
function traverseNode(node, parent) {
// 我們首先通過 type 看看 visitor 是否存在方法
let methods = visitor[node.type];
// 如果這個(gè)節(jié)點(diǎn)有 `enter` 方法, 我們就 enter 進(jìn)入
if (methods && methods.enter) {
methods.enter(node, parent);
}
// 接下來伍派,我們將按節(jié)點(diǎn) type 拆分
switch (node.type) {
// 從 level `Program` 開始, 它的 body 屬性的值是一個(gè) node 的 array,
// 我們用上面的方法遍歷
// traverseArray 將依次調(diào)用 `traverseNode`江耀,所以我們在通過遞歸對樹進(jìn)行遍歷
case 'Program':
traverseArray(node.body, node);
break;
// 接下來, 我們對 CallExpression 的參數(shù) params 做同樣的事
case 'CallExpression':
traverseArray(node.params, node);
break;
// 遇到?jīng)]有子節(jié)點(diǎn)的 `NumberLiteral` 和 `StringLiteral`,
// 直接 break 就可以了
case 'NumberLiteral':
case 'StringLiteral':
break;
// 不認(rèn)識的節(jié)點(diǎn), 直接報(bào)錯(cuò)
default:
throw new TypeError(node.type);
}
// 如果這個(gè)節(jié)點(diǎn)有 `exit` 方法, 我們就 exit 回退
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 調(diào)用 traverseNode, 傳入 AST, AST 的根節(jié)點(diǎn)沒有 parent 傳 null
return traverseNode(ast, null);
}
?(??????????)? the transformer !!!
接下來,transfromer诉植。
我們的 transformer 將基于我們剛剛建立的 AST 創(chuàng)建一個(gè)新的 AST.
----------------------------------------------------------------------------
Original AST Transformed AST
----------------------------------------------------------------------------
{ | {
type: 'Program', | type: 'Program',
body: [{ | body: [{
type: 'CallExpression', | type: 'ExpressionStatement',
name: 'add', | expression: {
params: [{ | type: 'CallExpression',
type: 'NumberLiteral', | callee: {
value: '2' | type: 'Identifier',
}, { | name: 'add'
type: 'CallExpression', | },
name: 'subtract', | arguments: [{
params: [{ | type: 'NumberLiteral',
type: 'NumberLiteral', | value: '2'
value: '4' | }, {
}, { | type: 'CallExpression',
type: 'NumberLiteral', | callee: {
value: '2' | type: 'Identifier',
}] | name: 'subtract'
}] | },
}] | arguments: [{
} | type: 'NumberLiteral',
| value: '4'
| }, {
| type: 'NumberLiteral',
| value: '2'
| }]
| }
| }
| }]
| }
----------------------------------------------------------------------------
所以, 我們的 transformer接收 lisp 的 AST
function transformer(ast) {
let newAst = {
type: 'Program',
body: [],
};
ast._context = newAst.body;
// 用 AST 和 visitor 調(diào)用 traverser
traverser(ast, {
// 第一個(gè) visitor 接收所有的 `NumberLiteral`
NumberLiteral: {
// 通過 enter 訪問
enter(node, parent) {
// 我們會(huì)創(chuàng)建一個(gè)新的節(jié)點(diǎn), 也叫做 NumberLiteral, 然后把它放入 parent context
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 接下來是 `StringLiteral`
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// `CallExpression`.
CallExpression: {
enter(node, parent) {
// 創(chuàng)建一個(gè)新節(jié)點(diǎn) `CallExpression` 并內(nèi)嵌一個(gè) `Identifier`.
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// 接下來我們在原來的 `CallExpression` 節(jié)點(diǎn)上,
//定義一個(gè) context 指向 expression 的參數(shù)
node._context = expression.arguments;
// 如果父節(jié)點(diǎn)不是 `CallExpression`
if (parent.type !== 'CallExpression') {
// 用 `ExpressionStatement` 包一下 `CallExpression`,
//因?yàn)?`CallExpression` 在 JS 中是真的語句
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
// 最后, 把 (包裝過的) `CallExpression` 放入 parent 的 context.
parent._context.push(expression);
},
}
});
// 返回剛剛生成的 new AST
return newAst;
}
ヾ(〃^?^)?? code generator
最后一步, Code Generator, 將以遞歸的方式打印上一步得到的 AST 的每個(gè)節(jié)點(diǎn).
function codeGenerator(node) {
// 通過node 的 type 屬性, 把 AST 轉(zhuǎn)為 code
switch (node.type) {
// 如果遇到 Program 節(jié)點(diǎn), 就遍歷 body 中的每個(gè)節(jié)點(diǎn),
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 對于 `ExpressionStatement`, 我們在內(nèi)嵌表達(dá)式中遞歸的使用代碼生成器, 結(jié)束后加上分號
case 'ExpressionStatement':
return (
codeGenerator(node.expression) + ';'
);
// 如果遇到 `CallExpression`, 就打印 `callee` 和左括號,
// 把 node 中的 arguments 映射成數(shù)組, 用逗號 join 一下, 最后打印一個(gè)右括號
case 'CallExpression':
return (
codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')'
);
// 遇到 `Identifier` 直接返回 `node`'s 名字.
case 'Identifier':
return node.name;
// 對于 `NumberLiteral` 直接返回 `node` 的值.
case 'NumberLiteral':
return node.value;
// 對 `StringLiteral` 給 `node` 的值加上引號.
case 'StringLiteral':
return '"' + node.value + '"';
// 對于其他節(jié)點(diǎn), 報(bào)錯(cuò)
default:
throw new TypeError(node.type);
}
}
點(diǎn)這里獲取代碼.
后面準(zhǔn)備逐一撕一下前端這些庫, 框架的面具, 都拾掇拾掇, 折騰折騰. 加油.
今天就到這里了.