在現今前端項目中,模塊化是一個避不開的話題。因此就會出現AMD,CMD等模塊加載方式。同時因爲JS不停的在更新迭代。出現不少實用的新語法。可是因爲有些語法有些超前,JS的宿主環境(瀏覽器/Node沒有跟上JS更新步驟),可是爲了在項目中使用這些好用到使人髮指的新特性,來提升開發效率等。就出現了各類前端編譯插件(Babel)。html
Babel is a JavaScript compiler前端
大多數編譯程序(compiler)分爲三個步驟:Parsing(分析階段)/Transformation(轉換)/Code Generation(代碼生成或者說生成目標代碼)node
天然語言 | 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)) |
Parsing 通常被分紅兩個步驟:Lexical Analysis(詞法分析)和 Syntactic Analysis(語法分析)編程
AST 是一個層級很深的對象。瀏覽器
e.g. 對(add 2 (subtract 4 2))進行Parsing處理。 Tokens以下(Note:其實Token是根據lexer生成的,不一樣的lexer處理結果是不同的。)bash
[
{ 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',
}]
}]
}]
}
複製代碼
transformation是compiler的第二個階段。他會接收通過SA處理生成的AST。在該階段可以利用一些語法規則,將AST轉換爲想被轉換的語言。模塊化
經過觀察AST會發現,每個elements(從AST角度看)或者token(從LA角度看)都有一個type屬性。這些element是屬於AST的Node結點。這些nodes經過對type屬性賦特定的值將AST劃分紅各自獨立的區塊。函數
e.g.requirejs
NumberLiteral 類型的Node
{
type: 'NumberLiteral',
value: '2',
}
複製代碼
CallExpression 類型的Node
{
type: 'CallExpression',
name: 'subtract',
params: [...內嵌的node邏輯...],
}
複製代碼
在transforming AST過程當中,咱們能夠經過adding/removing/replacing 屬性來修改nodes,同時咱們能夠add/remove nodes,甚至咱們能夠基於現有的AST來從新構建新的AST對象。
因爲咱們是須要將LISP語法的代碼轉換爲C的,因此咱們的關注點就是基於SA輸出的AST構建一個全新的適用於目標語言的AST對象。
爲了可以在transforming過程當中檢測這些nodes。同時因爲AST是一個層級很深的對象樹,因此須要對AST進行depth-first(深度優先遍歷)。(其實這和React在Render階段是同樣的)
{
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',
}]
}]
}]
}
複製代碼
對於上述的AST,在traversal階段,範圍每一個node的前後順序以下
爲了用代碼實現traversal過程,咱們構建一個內置可以接收不一樣node類型函數的"visitor"對象。
var visitor = {
NumberLiteral() {},
CallExpression() {},
};
複製代碼
當在遍歷AST的時候,在咱們訪問對應的node結點時,就會觸發與之類型匹配的visitor中的方法。
若是隻是單純的在訪問結點的時候觸發對應的方法,這種狀況是沒法紀錄訪問的"軌跡",因此須要對visitor進行改進。傳入被訪問的node結點,還有該node的直接父級結點。
var visitor = {
NumberLiteral(node, parent) {},
CallExpression(node, parent) {},
};
複製代碼
若是沒有返回處理,"遊標"在遍歷到最後的node就會中止,由於他不知道下一步該如何進行。
- Program
- CallExpression
- NumberLiteral
- CallExpression
- NumberLiteral
- NumberLiteral
複製代碼
因爲在遍歷AST的過程當中是採用depth-first的方式,就須要在訪問到最後的node的時候,須要按照原路返回,直到返回到起點,這樣才能被程序識別,這顆樹被遍歷完成了。
-> 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)
複製代碼
爲了實現上述邏輯,須要對visitor作額外的處理
var visitor = {
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
},
CallExpression:{
enter(node, parent){},
exit(node, parent){},
},
};
複製代碼
compiler的最後階段是code generation。有些compiler在CG階段作的工做會和transformation的重疊,可是大部分的CG的工做就是接收被處理過的AST而後將AST對象字符化(該操做相似於JSON.stringify(Object))。 一個高效的CG是可以根據AST不一樣的node type輸出對應的code,同時可以在樹內進行遞歸調用直到全部的node都被字符化。