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


























