瀏覽器的引擎編譯、執(zhí)行原理知多少?
本文轉(zhuǎn)載自微信公眾號「前端萬有引力」,作者一川 。轉(zhuǎn)載本文請聯(lián)系前端萬有引力公眾號。
1寫在前面
瀏覽器引擎是如何編譯、執(zhí)行JS代碼的呢?
本文將分析瀏覽器引起對JS代碼的編譯情況,并結(jié)合實際開發(fā)經(jīng)驗,重新理解底層的編譯解析機制,將有助于理解前端開發(fā)在跨端中的應(yīng)用以及一套代碼生成多端的底層邏輯。那么:
- Javascript代碼被執(zhí)行分為幾個階段呢?
- AST到底是做什么用的?
2V8引擎
我們知道程序語言分為編譯型語言和解釋型語言,他們各自的特點是:
- 編譯型語言:在代碼執(zhí)行前編譯器直接將對應(yīng)的代碼轉(zhuǎn)換為機器碼。如C++
- 解釋型語言:先將代碼轉(zhuǎn)換為編譯型代碼,再轉(zhuǎn)為機器碼,其是在運行時轉(zhuǎn)換的。如:Python、Javascript
為了提高運行效率,很多瀏覽器廠商在不斷努力,在現(xiàn)代瀏覽器中Chrome的v8引擎是最出類拔萃的,引入了Java虛擬機和C++編譯器的眾多技術(shù)。也正因此,Node.js也是基于V8引擎開發(fā)的。
那么v8引擎執(zhí)行JS代碼需要經(jīng)歷哪些階段,如下:
- Parse階段:v8引擎負(fù)責(zé)將JS代碼轉(zhuǎn)換為AST(抽象語法樹)
- Ignition階段:解釋器將AST轉(zhuǎn)換為字節(jié)碼,解析執(zhí)行字節(jié)碼,同時為下階段優(yōu)化編譯提供需要的信息
- TurboFan階段:編譯器利用上個階段收集的信息,將字節(jié)碼優(yōu)化為可以執(zhí)行的機器碼
- Orinoco階段:垃圾回收階段,將程序中不再使用的內(nèi)存空間進(jìn)行回收
編譯、執(zhí)行流程圖
AST
在計算機科學(xué)中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式,這里特指編程語言的源代碼。
在開發(fā)生產(chǎn)中,我們經(jīng)常使用eslint和babel這些工具都和AST有聯(lián)系,v8引擎就是通過編譯器將源代碼解析為AST的。常見的應(yīng)用場景有:
- JS反編譯,語法解析
- Babel編譯ES6語法
- 代碼高亮
- 關(guān)鍵字匹配
- 代碼壓縮
生成AST有兩個關(guān)鍵:詞法分析和語法分析 語法分析:這個階段會將源代碼拆分成最小的、不可再分的詞法單元,稱為token,代碼中的空格在JS中是直接忽略的。詞法單元之間都是獨立的,也即在該階段我們并不關(guān)心每一行代碼是通過什么方式組合在一起的。
語法分析:這個過程是將詞法單元轉(zhuǎn)換成一個由元素逐級嵌套所組成的待料了程序語法結(jié)構(gòu)的樹,被稱為抽象語法樹。將上一階段生成的 token 列表轉(zhuǎn)換為如下圖右側(cè)所示的 AST,根據(jù)這個數(shù)據(jù)結(jié)構(gòu)大致可以看出轉(zhuǎn)換之前源代碼的基本構(gòu)造。
簡而言之,詞法分析階段就是將代碼拆解成獨立的、不可再分的tokens,而語法分析階段就是將拆分后的tokens進(jìn)行解析,根據(jù)其在整個代碼上下文中的作用和聯(lián)系,去除多余的token形成抽象語法樹。
瀏覽器還不支持es6語法,需要將其轉(zhuǎn)換為es5語法,這個過程需要借助babel來實現(xiàn)。將es6源碼解析成AST,再將es6語法的抽象語法樹轉(zhuǎn)為es5的抽象語法樹,最后利用它來生成es5的源代碼。
生成字節(jié)碼
Ignition階段就是將AST轉(zhuǎn)換為字節(jié)碼,但是之前的v8版本不會經(jīng)歷此過程,最早只是直接通過AST轉(zhuǎn)成機器碼,后面的版本才開始對其進(jìn)行改進(jìn)。將AST直接轉(zhuǎn)為機器碼是存在問題的,因為:
- 直接轉(zhuǎn)換會帶來內(nèi)存占用過大的問題。因為將抽象語法樹全部生成了機器碼,而機器碼相比字節(jié)碼占用的內(nèi)存更多
- 某些JS使用場景使用解釋器更為合適。解析成字節(jié)碼,有些代碼沒必要解析成機器碼,進(jìn)而可以減少占用大量的內(nèi)存空間
V8引擎重新引進(jìn)Ignition解釋器,將抽象語法樹轉(zhuǎn)換成字節(jié)碼后,內(nèi)存占用顯著降低,同時可以使用JIT編譯器做進(jìn)一步優(yōu)化。
字節(jié)碼是介于AST和機器碼之間的代碼,需要將其轉(zhuǎn)換為機器碼后才能執(zhí)行,字節(jié)碼可以理解為機器碼的一種抽象。
解釋器在得到AST后,會按需進(jìn)行解釋和執(zhí)行,也就是說如果某個函數(shù)沒有被調(diào)用,則不會去解釋執(zhí)行它。
解釋器創(chuàng)建了調(diào)用棧來記錄函數(shù)的調(diào)用流程,每調(diào)用一個函數(shù),解釋器就會把該函數(shù)添加進(jìn)調(diào)用棧。解釋器會為被添加進(jìn)入的函數(shù)創(chuàng)建一個棧幀,這個棧幀是用來保存函數(shù)的局部變量以及執(zhí)行語句,因此會立即執(zhí)行這個棧幀。如果正在執(zhí)行的函數(shù)還調(diào)用了其他函數(shù),那么新函數(shù)也將會被添加進(jìn)調(diào)用棧并執(zhí)行,一旦這個函數(shù)執(zhí)行結(jié)束,對應(yīng)的棧幀就會被立即銷毀。查看調(diào)用棧的方式有兩種:調(diào)用函數(shù)console.log()打印到控制臺,利用瀏覽器開發(fā)者工具進(jìn)行斷點調(diào)試。
生成機器碼
如果發(fā)現(xiàn)一段代碼被重復(fù)執(zhí)行多次的情況,生成的字節(jié)碼以及分析數(shù)據(jù)會傳給TurboFan編譯器,它會根據(jù)分析數(shù)據(jù)的情況生成優(yōu)化好的機器碼。
TurboFan編譯器是JIT優(yōu)化的編譯器,TurboFan的編譯線程和生成字節(jié)碼不會在同一個線程上,這樣可以和Ignition解釋器相互配合著使用,不受另外一方的影響。由Ignition解釋器收集的分析數(shù)據(jù)被TurboFan編譯器使用,主要是通過一種推測優(yōu)化的技術(shù),生成已經(jīng)優(yōu)化的機器碼進(jìn)行執(zhí)行。
優(yōu)化后的機器碼作用與緩存很類似,當(dāng)解釋器再次遇到相同的內(nèi)容時,就可以直接執(zhí)行優(yōu)化后的機器碼。當(dāng)然優(yōu)化后的代碼有可能會無法運行(比如函數(shù)參數(shù)類型改變),那么會再次反優(yōu)化為字節(jié)碼交給解釋器。
3參考文章
《Javascript核心原理精講》
《前端也要懂編譯:AST 從入門到上手指南》
《編譯原理》
4寫在最后
當(dāng)前市面上比較主流的JS引擎編譯過程大部分類似,主要原因可能是在某些地方加入了特定的優(yōu)化,但是其核心思路和v8大體差不多。AST是比較重要的知識點,深入了解之后有助于自己實現(xiàn)前端工具。對此可以通過多研究一些前端工具,來提升自己的業(yè)務(wù)開發(fā)效率和編程能力。