Babel是前端很常用的轉(zhuǎn)碼器,更準(zhǔn)確地說是轉(zhuǎn)譯器,是從源碼到源碼的轉(zhuǎn)換編譯器,例如可以將我們按照ES6標(biāo)準(zhǔn)寫的代碼轉(zhuǎn)為ES5標(biāo)準(zhǔn),也就是說可以直接使用ES6的最新標(biāo)準(zhǔn)來編寫腳本,而不用擔(dān)心現(xiàn)有環(huán)境是否支持此標(biāo)準(zhǔn)。
例如:Babel可以將我們最常用的箭頭函數(shù):
const demo = item => item + 1;
轉(zhuǎn)譯成ES5的函數(shù)寫法:
const demo = function(item){
return item + 1;
}
將ES6標(biāo)準(zhǔn)轉(zhuǎn)譯成ES5,不用擔(dān)心各大瀏覽器是否已經(jīng)支持ES6的最新標(biāo)準(zhǔn)這個確實(shí)是解決了我們工作中一大問題,但是Babel的功能并不止于此,它是可以轉(zhuǎn)譯很多種語法的,例如我們最常用的react中JSX的語法,而且Babel的核心就是利用插件,通過不同的插件可以轉(zhuǎn)譯不同的語法,讓我們可以暢快的去嘗試最新的語法。
Babel的三大主要步驟
我們先來看一張圖,這是我網(wǎng)上找來的,我感覺這張圖已經(jīng)把Babel的工作步驟畫的很清楚了。

從上圖可以看出Babel的三大步驟分別是:解析(parse),轉(zhuǎn)換(transform),生成(generate)。嘿嘿,我發(fā)現(xiàn)圖中的英文有點(diǎn)問題,大家可以查一下是不是,如果是我翻譯錯了請指正。
什么是AST?
在詳細(xì)解釋這三大步驟前,我們有必要先來了解一下什么是AST?
“ AST ”其實(shí)叫做“ 抽象語法樹 ”,是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式,其實(shí)個人覺得babel對于AST和我們熟悉的jquery對于DOM有點(diǎn)像。我們可以想象一下如何將JS代碼用樹狀表示出來。
var a = 1 + 1
var b = 2 + 2
上面聲明了兩個變量,如何用樹狀表示他們呢?首先一定會有東西可以代表這些聲明、變量名、常量等等的信息。很明顯,這棵樹上有兩個變量,兩個變量名a和b,有兩個運(yùn)算語句,操作符都是+號。但是有了這些還不夠,既然是樹,樹枝連樹枝,還必須建立起彼此之間的關(guān)系,比如一個聲明語句,聲明類型是var,左側(cè)是變量名,右側(cè)是表達(dá)式,有了這些信息我們就可以還原這個程序了。這個就是把源碼解析成AST時所做的事情了。
在AST中我們用node(節(jié)點(diǎn))來表示每個代碼片段,比如上面程序的整體就是一個節(jié)點(diǎn)(Program,所有的AST根節(jié)點(diǎn)都是Program節(jié)點(diǎn)),然后下面有兩條語句,所以它的body屬性上就兩個聲明節(jié)點(diǎn)VariableDeclaration。所以上面程序的AST類似這樣:

從圖上可以看出節(jié)點(diǎn)上用了各個屬性來表示各種信息以及程序之間的關(guān)系。
解析(parse):
在大概了解了AST是個啥東西后,我們可以來了解三大步驟了,首先是第一步解析。主要是為了接收代碼并輸出AST,也就是將代碼變?yōu)闃錉?,這個步驟又分兩個階段:詞法分析(Lexical Analysis)和 語法分析(Syntactic Analysis)。
詞法分析
詞法分析階段是把字符串形式的代碼轉(zhuǎn)換成令牌(tokens)流。你可以把tokens看成是一個語法片段數(shù)組。例如:n*n代碼經(jīng)過詞法分析階段后轉(zhuǎ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)流轉(zhuǎn)換成AST的形式,以便于后續(xù)的操作。
轉(zhuǎn)換(transform)
第二步是轉(zhuǎn)換,主要是用來接收解析好的AST并對其進(jìn)行遍歷,在此過程中對節(jié)點(diǎn)進(jìn)行添加、更新或者移除等操作,準(zhǔn)確的說是利用我們配置好的plugins/presets把Parser生成的AST轉(zhuǎn)變?yōu)樾碌腁ST,這一步是插件要介入工作的部分,從三大步驟的圖中也可以看出占了很大一塊的比重,足以看出這個轉(zhuǎn)換過程就是Babel中最復(fù)雜的部分,我們平時配置的plugins/presets就是作用在這里了。
生成(generate)
第三步是生成,最后一步把最后經(jīng)過一系列轉(zhuǎn)換后的最新AST轉(zhuǎn)換成字符串形式的代碼,同時還會創(chuàng)建源碼映射(source maps)。代碼生成反而比較簡單,只要深度優(yōu)先遍歷整個AST,然后構(gòu)建可以表示轉(zhuǎn)換后代碼的字符串就可以了。
Visitors(訪問者)
當(dāng)我們說到“進(jìn)入”一個節(jié)點(diǎn)時,其實(shí)是在說我們在訪問它,之所以使用這樣的術(shù)語是因?yàn)橛幸粋€訪問者模式的概念。
這里的訪問者是一個用于AST遍歷的跨語言的模式。簡單來說就是一個對象,定義了用于在一個樹狀結(jié)構(gòu)中獲取具體節(jié)點(diǎn)的方法,例如:
const MyVisitor = {
Identifier: {
// 當(dāng)進(jìn)入Identifier節(jié)點(diǎn)的時候執(zhí)行
enter() {
console.log("Entered");
},
// 當(dāng)退出Identifier節(jié)點(diǎn)的時候執(zhí)行
exit() {
console.log("Exited!");
}
}
};
每一個節(jié)點(diǎn)都會有自己對應(yīng)的type,比如變量節(jié)點(diǎn)Identifier等。上例中我們給babel提供了一個MyVisitor對象,在這個對象上面我們以這些節(jié)點(diǎn)的type做為key,已一個函數(shù)作為值,這樣在遍歷進(jìn)入到對應(yīng)節(jié)點(diǎn)時,babel就會執(zhí)行對應(yīng)的enter函數(shù),向上遍歷退出對應(yīng)節(jié)點(diǎn)時,babel就會去執(zhí)行對應(yīng)的exit函數(shù)。
Paths(路徑)
我們通過visitor可以在遍歷到對應(yīng)節(jié)點(diǎn)執(zhí)行對應(yīng)的函數(shù),可是要修改對應(yīng)節(jié)點(diǎn)的信息,還是不夠,畢竟要增刪節(jié)點(diǎn),我們不能等進(jìn)入節(jié)點(diǎn)了才執(zhí)行,我們還需要拿到對應(yīng)節(jié)點(diǎn)的信息以及節(jié)點(diǎn)和所在的位置(即和其他節(jié)點(diǎn)間的關(guān)系), visitor在遍歷到對應(yīng)節(jié)點(diǎn)執(zhí)行對應(yīng)函數(shù)時候會給我們傳入path參數(shù),輔助我們完成上面這些操作。Path 是表示兩個節(jié)點(diǎn)之間連接的對象,而不是當(dāng)前節(jié)點(diǎn),我們上面訪問到了Identifier節(jié)點(diǎn),它傳入的 path參數(shù)看起來是這樣的:
{
"parent": {
"type": "VariableDeclarator",
"id": {
...
},
....
},
"node": {
"type": "Identifier",
"name": "..."
}
從上例可以看出:path.node.name可以獲得當(dāng)前節(jié)點(diǎn)的name,path.parent.id可以獲得父節(jié)點(diǎn)的id,另外path對象上面還包含了添加、更新、移動和刪除節(jié)點(diǎn)有關(guān)的很多方法,至于這些有關(guān)的方法就不再這里展開了,可以看文檔解決。上面說visitor在遍歷到對應(yīng)節(jié)點(diǎn)執(zhí)行對應(yīng)函數(shù)時候會給我們傳入path參數(shù),所以我們可以根據(jù)這個修改一下上文中的MyVisitor函數(shù):
const MyVisitor = {
Identifier: {
// 當(dāng)進(jìn)入Identifier節(jié)點(diǎn)的時候執(zhí)行
enter(path) {
console.log('traverse enter a Identifier node the name is ' + path.node.name);
},
// 當(dāng)退出Identifier節(jié)點(diǎn)的時候執(zhí)行
exit(path) {
console.log('traverse exit a Identifier node the name is ' + path.node.name);
}
}
};
這樣我們就可以操作想要改變的節(jié)點(diǎn)了,嗯嗯~~ very good?。?/p>
總結(jié)
最后我們總結(jié)一下,Babel最重要的就是熟悉它的工作步驟,也就是它的原理:
- 接收源代碼
- 將源代碼轉(zhuǎn)成字符串形式
- 把字符串形式的源代碼轉(zhuǎn)換成令牌流
- 把一個令牌流轉(zhuǎn)換成AST的樹狀形式。
- 接收AST并對其進(jìn)行遍歷,在此過程中可以對節(jié)點(diǎn)進(jìn)行各 種操作,比如添加、更新、移除等等。
- 深度遍歷最終的AST樹,然后構(gòu)建可以表示轉(zhuǎn)換后代碼的字符串,并且同時創(chuàng)建源代碼映射。
其實(shí)很多猿兄都和我一樣,剛接觸babel的時候,直接上手用,看著文檔知道如何用,但是不知背后的原理,今天這一片筆記也是看了好幾篇文檔和大牛的博客整理出來的比較關(guān)鍵的幾點(diǎn),看上去簡簡單單的轉(zhuǎn)譯器,其實(shí)背后的實(shí)現(xiàn)還是挺不容易的,我們已經(jīng)簡單的分析了代碼,并且可以修改一些抽象語法樹上的內(nèi)容來達(dá)到我們的目的,不過開頭的時候也說了對于Babel而言插件是很重要的,現(xiàn)階段Babel已經(jīng)不僅僅是去轉(zhuǎn)換ES6了,最常用的還有轉(zhuǎn)換react中JSX的語法,所以除了懂得原理以外,我們也可以自己實(shí)際去編寫一些有意思的插件來應(yīng)用與自己的工作中,更好的提高對Babel的理解,今天介紹就到這里,還是那句話如有總結(jié)不到位的,希望各位猿兄指教。