Babel是前端很常用的轉碼器,更準確地說是轉譯器,是從源碼到源碼的轉換編譯器,例如可以將我們按照ES6標準寫的代碼轉為ES5標準,也就是說可以直接使用ES6的最新標準來編寫腳本,而不用擔心現有環境是否支持此標準。
例如:Babel可以將我們最常用的箭頭函數:
const demo = item => item + 1;
轉譯成ES5的函數寫法:
const demo = function(item){
return item + 1;
}
將ES6標準轉譯成ES5,不用擔心各大瀏覽器是否已經支持ES6的最新標準這個確實是解決了我們工作中一大問題,但是Babel的功能并不止于此,它是可以轉譯很多種語法的,例如我們最常用的react中JSX的語法,而且Babel的核心就是利用插件,通過不同的插件可以轉譯不同的語法,讓我們可以暢快的去嘗試最新的語法。
Babel的三大主要步驟
我們先來看一張圖,這是我網上找來的,我感覺這張圖已經把Babel的工作步驟畫的很清楚了。
從上圖可以看出Babel的三大步驟分別是:解析(parse),轉換(transform),生成(generate)。嘿嘿,我發現圖中的英文有點問題,大家可以查一下是不是,如果是我翻譯錯了請指正。
什么是AST?
在詳細解釋這三大步驟前,我們有必要先來了解一下什么是AST?
“ AST ”其實叫做“ 抽象語法樹 ”,是源代碼的抽象語法結構的樹狀表現形式,其實個人覺得babel對于AST和我們熟悉的jquery對于DOM有點像。我們可以想象一下如何將JS代碼用樹狀表示出來。
var a = 1 + 1
var b = 2 + 2
上面聲明了兩個變量,如何用樹狀表示他們呢?首先一定會有東西可以代表這些聲明、變量名、常量等等的信息。很明顯,這棵樹上有兩個變量,兩個變量名a和b,有兩個運算語句,操作符都是+號。但是有了這些還不夠,既然是樹,樹枝連樹枝,還必須建立起彼此之間的關系,比如一個聲明語句,聲明類型是var,左側是變量名,右側是表達式,有了這些信息我們就可以還原這個程序了。這個就是把源碼解析成AST時所做的事情了。
在AST中我們用node(節點)來表示每個代碼片段,比如上面程序的整體就是一個節點(Program,所有的AST根節點都是Program節點),然后下面有兩條語句,所以它的body屬性上就兩個聲明節點VariableDeclaration。所以上面程序的AST類似這樣:
從圖上可以看出節點上用了各個屬性來表示各種信息以及程序之間的關系。
解析(parse):
在大概了解了AST是個啥東西后,我們可以來了解三大步驟了,首先是第一步解析。主要是為了接收代碼并輸出AST,也就是將代碼變為樹狀,這個步驟又分兩個階段:詞法分析(Lexical Analysis)和 語法分析(Syntactic Analysis)。
詞法分析
詞法分析階段是把字符串形式的代碼轉換成令牌(tokens)流。你可以把tokens看成是一個語法片段數組。例如:n*n代碼經過詞法分析階段后轉換成了tokens:
// n*n
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
...
]
每一個type又有一組屬性來描述該令牌:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
語法分析
語法分析階段是把一個令牌(tokens)流轉換成AST的形式,以便于后續的操作。
轉換(transform)
第二步是轉換,主要是用來接收解析好的AST并對其進行遍歷,在此過程中對節點進行添加、更新或者移除等操作,準確的說是利用我們配置好的plugins/presets把Parser生成的AST轉變為新的AST,這一步是插件要介入工作的部分,從三大步驟的圖中也可以看出占了很大一塊的比重,足以看出這個轉換過程就是Babel中最復雜的部分,我們平時配置的plugins/presets就是作用在這里了。
生成(generate)
第三步是生成,最后一步把最后經過一系列轉換后的最新AST轉換成字符串形式的代碼,同時還會創建源碼映射(source maps)。代碼生成反而比較簡單,只要深度優先遍歷整個AST,然后構建可以表示轉換后代碼的字符串就可以了。
Visitors(訪問者)
當我們說到“進入”一個節點時,其實是在說我們在訪問它,之所以使用這樣的術語是因為有一個訪問者模式的概念。
這里的訪問者是一個用于AST遍歷的跨語言的模式。簡單來說就是一個對象,定義了用于在一個樹狀結構中獲取具體節點的方法,例如:
const MyVisitor = {
Identifier: {
// 當進入Identifier節點的時候執行
enter() {
console.log("Entered");
},
// 當退出Identifier節點的時候執行
exit() {
console.log("Exited!");
}
}
};
每一個節點都會有自己對應的type,比如變量節點Identifier等。上例中我們給babel提供了一個MyVisitor對象,在這個對象上面我們以這些節點的type做為key,已一個函數作為值,這樣在遍歷進入到對應節點時,babel就會執行對應的enter函數,向上遍歷退出對應節點時,babel就會去執行對應的exit函數。
Paths(路徑)
我們通過visitor可以在遍歷到對應節點執行對應的函數,可是要修改對應節點的信息,還是不夠,畢竟要增刪節點,我們不能等進入節點了才執行,我們還需要拿到對應節點的信息以及節點和所在的位置(即和其他節點間的關系), visitor在遍歷到對應節點執行對應函數時候會給我們傳入path參數,輔助我們完成上面這些操作。Path 是表示兩個節點之間連接的對象,而不是當前節點,我們上面訪問到了Identifier節點,它傳入的 path參數看起來是這樣的:
{
"parent": {
"type": "VariableDeclarator",
"id": {
...
},
....
},
"node": {
"type": "Identifier",
"name": "..."
}
從上例可以看出:path.node.name可以獲得當前節點的name,path.parent.id可以獲得父節點的id,另外path對象上面還包含了添加、更新、移動和刪除節點有關的很多方法,至于這些有關的方法就不再這里展開了,可以看文檔解決。上面說visitor在遍歷到對應節點執行對應函數時候會給我們傳入path參數,所以我們可以根據這個修改一下上文中的MyVisitor函數:
const MyVisitor = {
Identifier: {
// 當進入Identifier節點的時候執行
enter(path) {
console.log('traverse enter a Identifier node the name is ' + path.node.name);
},
// 當退出Identifier節點的時候執行
exit(path) {
console.log('traverse exit a Identifier node the name is ' + path.node.name);
}
}
};
這樣我們就可以操作想要改變的節點了,嗯嗯~~ very good!!
總結
最后我們總結一下,Babel最重要的就是熟悉它的工作步驟,也就是它的原理:
- 接收源代碼
- 將源代碼轉成字符串形式
- 把字符串形式的源代碼轉換成令牌流
- 把一個令牌流轉換成AST的樹狀形式。
- 接收AST并對其進行遍歷,在此過程中可以對節點進行各 種操作,比如添加、更新、移除等等。
- 深度遍歷最終的AST樹,然后構建可以表示轉換后代碼的字符串,并且同時創建源代碼映射。
其實很多猿兄都和我一樣,剛接觸babel的時候,直接上手用,看著文檔知道如何用,但是不知背后的原理,今天這一片筆記也是看了好幾篇文檔和大牛的博客整理出來的比較關鍵的幾點,看上去簡簡單單的轉譯器,其實背后的實現還是挺不容易的,我們已經簡單的分析了代碼,并且可以修改一些抽象語法樹上的內容來達到我們的目的,不過開頭的時候也說了對于Babel而言插件是很重要的,現階段Babel已經不僅僅是去轉換ES6了,最常用的還有轉換react中JSX的語法,所以除了懂得原理以外,我們也可以自己實際去編寫一些有意思的插件來應用與自己的工作中,更好的提高對Babel的理解,今天介紹就到這里,還是那句話如有總結不到位的,希望各位猿兄指教。