瀏覽器如何運(yùn)行一段JavaScript代碼
開篇
JavaScript在我們?nèi)粘i_發(fā)中扮演著至關(guān)重要的角色,大部分時(shí)間都與它打交道,因此對這門語言的了解再多也不為過。
雖然有很多關(guān)于JavaScript的書籍和資料,但絕大多數(shù)都從JavaScript語言本身的角度去分析其語法和語義,很少有與JavaScript具體執(zhí)行過程相關(guān)的分析資料。因此,我邀請大家一起從瀏覽器的角度來看一下一段JavaScript代碼到底是如何執(zhí)行的。
以 Google 瀏覽器為例,JavaScript 代碼由 V8 引擎負(fù)責(zé)解釋執(zhí)行。代碼的執(zhí)行總體上經(jīng)歷以下三個(gè)步驟:
- 將代碼解析為抽象語法樹(AST)并創(chuàng)建執(zhí)行上下文
- 根據(jù) AST 生成字節(jié)碼作為中間代碼
- 解釋器
Ignition會逐條解釋執(zhí)行代碼
整個(gè)流程圖如下:

生成AST
為什么要將代碼解析成AST呢,直接用原代碼不行嗎?
答案肯定是不行,因?yàn)闉g覽器并不能理解我們所寫的代碼。這個(gè)理由就像將html需要解析為DOM一樣,瀏覽器并不能理解我們所編寫的代碼,需要將我們寫的代碼編譯成瀏覽器能夠理解的結(jié)構(gòu),也就是AST。
AST是一種非常重要的數(shù)據(jù)結(jié)構(gòu),許多著名的項(xiàng)目都在使用它。
比如Babel,Babel被用于代碼轉(zhuǎn)換,將es6的代碼轉(zhuǎn)換為es5的代碼,用于解決兼容性問題,Babel的工作原理就是將es6源碼解析為AST,再將es6的AST轉(zhuǎn)化為es5的AST,最后利用es5的AST生成源碼。
類似的還有ESlint,ESlint是用來檢查js代碼規(guī)范的插件,它的檢查流程也是將源碼解析成AST,再利用AST來檢查代碼的規(guī)范問題。
將源碼解析成AST通常需要兩個(gè)步驟:
一、詞法分析
詞法分析又稱為分詞(tokenize),是將一行行代碼拆分為一個(gè)個(gè)token。如下圖:

二、語法分析
語法分析又稱解析,將上一步生成的token數(shù)據(jù)根據(jù)語法規(guī)則轉(zhuǎn)化為AST。在轉(zhuǎn)化過程中,如果不符合語法規(guī)則,會終止轉(zhuǎn)化并拋出一個(gè)語法錯(cuò)誤。轉(zhuǎn)化成的AST如下圖:

可以通過 AST可視化網(wǎng)站:https://astexplorer.net/ 來體驗(yàn)成生成AST。
除了生成AST,該階段還會創(chuàng)建代碼塊的執(zhí)行上下文。其實(shí)上述的編譯過程都發(fā)生在v8引擎內(nèi)部,對于我們開發(fā)者來說完全是黑盒的,但是執(zhí)行上下文這個(gè)概念卻是至關(guān)重要的,因?yàn)樗谴a運(yùn)行的基本環(huán)境。生成字節(jié)碼
生成AST后,接著解釋器 Ignition 就會將AST轉(zhuǎn)化為字節(jié)碼并解釋執(zhí)行字節(jié)碼。
為什么又要將AST解析成字節(jié)碼呢,上文不是說過瀏覽器可以理解AST嗎?答案是解決內(nèi)存占用的問題。
其實(shí)一開始v8引擎并不會將AST轉(zhuǎn)化為字節(jié)碼,而是直接將AST轉(zhuǎn)化為機(jī)器碼并執(zhí)行。雖然執(zhí)行效率特別高,但是同時(shí)暴露出來了嚴(yán)重的內(nèi)存占用問題。因?yàn)関8引擎要消耗大量的內(nèi)存來存放轉(zhuǎn)化后的機(jī)器碼。為了解決內(nèi)存占用的問題,v8團(tuán)隊(duì)引入了字節(jié)碼,花了大量的時(shí)間,完成了當(dāng)前的這套架構(gòu)。
解釋下字節(jié)碼是什么,字節(jié)碼是介于AST和機(jī)器碼之間的一種代碼,字節(jié)碼需要解釋器將其轉(zhuǎn)化為機(jī)器碼之后才能執(zhí)行。為了更好的去理解字節(jié)碼,下面是源碼、字節(jié)碼、機(jī)器碼的一個(gè)對比圖:

可以看出,字節(jié)碼對比機(jī)器碼占用的空間要小得多。所以使用字節(jié)碼可以減少系統(tǒng)的內(nèi)存使用。
執(zhí)行代碼
對于一段第一次被執(zhí)行的字節(jié)碼,解釋器 Ignition 會逐條解釋并執(zhí)行。在執(zhí)行的過程中,如果發(fā)現(xiàn)熱點(diǎn)代碼(Hotspot),比如說一段被重復(fù)執(zhí)行多次的代碼,后臺的 編譯器 TurboFan 就會將該段熱點(diǎn)字節(jié)碼編譯成機(jī)器碼并保存起,當(dāng)下次在執(zhí)行到該段熱點(diǎn)代碼時(shí),只需要執(zhí)行編譯后的機(jī)器碼就行,這樣就節(jié)提升了大量的執(zhí)行效率。執(zhí)行流程如下圖所示:

這種字節(jié)碼配合解釋器和編譯器的技術(shù)正是最近很火的技術(shù)————即時(shí)編譯(JIT) 。
總結(jié)
至此本文分析完了 JavaScript 代碼執(zhí)行的整個(gè)階段。在宏觀角度上 JavaScript 代碼會經(jīng)歷如下步驟:
- 解析器將 JavaScript 代碼解析(詞法分析、語法分析)成AST并創(chuàng)建執(zhí)行上下文
- 解釋器 Ignition 將AST轉(zhuǎn)化為字節(jié)碼
- 解釋器 Ignition 對字節(jié)碼逐條解釋執(zhí)行
- 如果發(fā)現(xiàn)熱點(diǎn)代碼(HotSpot),后臺編譯器 TurboFan 會將熱點(diǎn)代碼編譯成機(jī)器碼并保存,進(jìn)而提升執(zhí)行效率。
整個(gè)JavaScript代碼執(zhí)行是慢啟動,越執(zhí)行越快。這種字節(jié)碼配合解釋器和編譯器的技術(shù)叫做即時(shí)編譯(JIT)。v8引擎也是基于這種技術(shù)來實(shí)現(xiàn)對內(nèi)存占用和執(zhí)行效率的調(diào)控。
最后在JavaScript代碼執(zhí)行過程中,最重要的部分就是執(zhí)行上下文了,它是代碼運(yùn)行時(shí)的基本環(huán)境。
參考鏈接
-
瀏覽器工作原理與實(shí)踐:https://blog.poetries.top/browser-working-principle/guide/part2/lesson07.html#%E5%8F%98%E9%87%8F%E6%8F%90%E5%8D%87-hoisting
-
How JavaScript Works:https://www.freecodecamp.org/news/javascript-under-the-hood-v8/
更多推薦

