深入瀏覽器工作原理和JS引擎(V8引擎為例)
瀏覽器工作原理和JS引擎
1.瀏覽器工作原理
在瀏覽器中輸入查找內容,瀏覽器是怎樣將頁面加載出來的?以及JavaScript代碼在瀏覽器中是如何被執行的?
大概流程可觀察以下圖:
- 首先,用戶在瀏覽器搜索欄中輸入服務器地址,與服務器建立連接;
- 服務器返回對應的靜態資源(一般為
index.html
); - 然後,瀏覽器拿到
index.html
後對其進行解析; - 當解析時遇到css或js文件,就向服務器請求並下載對應的css文件和js文件;
- 最後,瀏覽器對頁面進行渲染,執行js代碼;
那麼在輸入服務器地址,敲下回車那一刻會發生什麼?
- 對瀏覽器輸入的地址進行DNS解析,將域名解析成對應的IP地址;
- 然後向這個IP地址發送http請求,服務器收到發送的http請求,處理並響應;
- 最終瀏覽器得到瀏覽器響應的內容;
2.瀏覽器的內核
瀏覽器從服務器下載的文件最終要進行解析,那麼內部是誰在幫助解析呢?這裡就涉及到瀏覽器內核。不同的瀏覽器由不同的內核構成,以下是幾個常見的瀏覽器內核:
- Gecko:早期被Netscape和Mozilla Firefox瀏覽器使用過;
- Trident:由微軟開發的,IE瀏覽器一直在使用,但Edge瀏覽器內核已經轉向了Blink;
- Webkit:蘋果基於KHTML開發,並且是開源的,用於Safari,Google Chrome瀏覽器早期也在使用;
- Blink:Google基於Webkit開發的,是Webkit的一個分支,目前應用於Google Chrome、Edge、Opera等等;
事實上,瀏覽器內核指的是瀏覽器的排版引擎(layout engine),也稱為瀏覽器引擎、頁面渲染引擎或樣版引擎。
3.瀏覽器的渲染過程
瀏覽器從服務器下載完文件後,就需要對其進行解析和渲染,流程如下:
- HTML Parser將HTML解析轉換成DOM樹;
- CSS Parser將樣式表解析轉換成CSS規則樹;
- 轉換完成的DOM樹和CSS規則樹Attachment(附加)在一起,並生成一個Render Tree(渲染樹);
- 需要注意的是,在生成Render Tree並不會立即進行繪製,中間還會有一個Layout(布局)操作,也就是布局引擎;
- 為什麼需要布局引擎再對Render Tree進行操作?因為不同時候瀏覽器所處的狀態是不一樣的(比如瀏覽器寬度),Layout的作用就是確定元素具體的展示位置和展示效果;
- 有了最終的Render Tree,瀏覽器就進行Painting(繪製),最後進行Display展示;
- 可以發現圖中還有一個紫色的DOM三角,實際上這裡是js對DOM的相關操作;
- 在HTML解析時,如果遇到JavaScript標籤,就會停止解析HTML,而去加載和執行JavaScript代碼;
那麼,JavaScript代碼由誰來執行呢?下面該JavaScript引擎出場了。
4.JavaScript引擎
首先由兩個問題來認識一下JavaScript引擎。
(1)為什麼需要JavaScript引擎?
- 首先,我們需要知道JavaScript是一門高級編程語言,所有的高級編程語言都是需要轉換成最終的機器指令來執行的;
- 而我們知道編寫的JS代碼可以由瀏覽器或者Node執行,其底層最終都是交給CPU執行;
- 但是CPU只認識自己的指令集,也就是機器語言,而JavaScript引擎主要功能就是幫助我們將JavaScript代碼翻譯CPU所能認識指令,最終被CPU執行;
(2)JavaScript引擎有哪些?
- SpiderMonkey:第一款JavaScript引擎,由Brendan Eich開發(JavaScript作者);
- Chakra:用於IE瀏覽器,由微軟開發;
- JavaScriptCore:Webkit中內置的JavaScript引擎,由蘋果公司開發;
- V8:目前最為強大和流行的JavaScript引擎,由Google開發;
5.瀏覽器內核和JS引擎的關係
這裡以Webkit內核為例。
-
實際上,Webkit由兩部分組成:
- WebCore:負責HTML解析、布局、渲染等相關的操作;
- JavaScriptCore(JSCore):解析和執行JavaScript代碼;
-
小程序中編寫的JavaScript代碼就是由JSCore執行的,也就是小程序使用的引擎就是JavaScriptCore:
-
渲染層:由Webview來解析和渲染wxml、wxss等;
-
邏輯層:由JSCore來解析和執行JS代碼;
-
以下為小程序的官方架構圖:
-
6.V8引擎
下面一起深入了解一下強大的V8引擎。
6.1.V8引擎的原理
先了解一下官方對V8引擎的定義:
-
V8引擎使用C++編寫的Google開源高性能JavaScript和WebAssembly引擎,它用於Chrome和Node.js等,可以獨立運行,也可以嵌入到任何C++的應用程序中。。
-
所以說V8並不單單只是服務於JavaScript的,還可以用於WebAssembly(一種用於基於堆棧的虛擬機的二進制指令格式),並且可以運行在多個平台。
-
下圖簡單的展示了V8的底層架構:
6.2.V8引擎的架構
V8的底層架構主要有三個核心模塊(Parse、Ignition和TurboFan),接下來對上面架構圖進行詳細說明。
(1)Parse模塊:將JavaScript代碼轉換成AST(抽象語法樹)。
-
該過程主要對JavaScript源代碼進行詞法分析和語法分析;
-
詞法分析:對代碼中的每一個詞或符號進行解析,最終會生成很多tokens(一個數組,裏面包含很多對象);
-
比如,對
const name = 'curry'
這一行代碼進行詞法分析:// 首先對const進行解析,因為const為一個關鍵字,所以類型會被記為一個關鍵詞,值為const tokens: [ { type: 'keyword', value: 'const' } ] // 接着對name進行解析,因為name為一個標識符,所以類型會被記為一個標識符,值為name tokens: [ { type: 'keyword', value: 'const' }, { type: 'identifier', value: 'name' } ] // 以此類推...
-
-
語法分析:在詞法分析的基礎上,拿到tokens中的一個個對象,根據它們不同的類型再進一步分析具體語法,最終生成AST;
-
以上即為簡單的JS詞法分析和語法分析過程介紹,如果想詳細查看我們的JavaScript代碼在通過Parse轉換後的AST,可以使用AST Explorer工具:
-
AST在前端應用場景特別多,比如將TypeScript代碼轉成JavaScript代碼、ES6轉ES5、還有像vue中的template等,都是先將其轉換成對應的AST,然後再生成目標代碼;
-
參考官方文檔://v8.dev/blog/scanner
(2)Ignition模塊:一個解釋器,可以將AST轉換成ByteCode(位元組碼)。
- 位元組碼(Byte-code):是一種包含執行程序,由一序列 op 代碼/數據對組成的二進制文件,是一種中間碼。
- 將JS代碼轉成AST是便於引擎對其進行操作,前面說到JS代碼最終是轉成機器碼給CPU執行的,為什麼還要先轉換成位元組碼呢?
- 因為JS運行所處的環境是不一定的,可能是windows或Linux或iOS,不同的操作系統其CPU所能識別的機器指令也是不一樣的。位元組碼是一種中間碼,本身就有跨平台的特性,然後V8引擎再根據當前所處的環境將位元組碼編譯成對應的機器指令給當前環境的CPU執行。
- 參考官方文檔://v8.dev/blog/ignition-interpreter
(3)TurboFan模塊:一個編譯器,可以將位元組碼編譯為CPU認識的機器碼。
- 在了解TurboFan模塊之前可以先考慮一個問題,如果每執行一次代碼,就要先將AST轉成位元組碼然後再解析成機器指令,是不是有點損耗性能呢?強大的V8早就考慮到了,所以出現了TurboFan這麼一個庫;
- TurboFan可以獲取到Ignition收集的一些信息,如果一個函數在代碼中被多次調用,那麼就會被標記為熱點函數,然後經過TurboFan轉換成優化的機器碼,再次執行該函數的時候就直接執行該機器碼,提高代碼的執行性能;
- 圖中還存在一個
Deoptimization
過程,其實就是機器碼被還原成ByteCode,比如,在後續執行代碼的過程中傳入熱點函數的參數類型發生了變化(如果給sum函數傳入number類型的參數,那麼就是做加法;如果給sum函數傳入String類型的參數,那麼就是做字符串拼接),可能之前優化的機器碼就不能滿足需求了,就會逆向轉成位元組碼,位元組碼再編譯成正確的機器碼進行執行; - 從這裡就可以發現,如果在編寫代碼時給函數傳遞固定類型的參數,是可以從一定程度上優化我們代碼執行效率的,所以TypeScript編譯出來的JavaScript代碼的性能是比較好的;
- 參考官方文檔://v8.dev/blog/turbofan-jit
6.3.V8引擎執行過程
V8引擎的官方在Parse過程提供了以下這幅圖,最後就來詳細了解一下Parse具體的執行過程。
- ①Blink內核將JS源碼交給V8引擎;
- ②Stream獲取到JS源碼進行編碼轉換;
- ③Scanner進行詞法分析,將代碼轉換成tokens;
- ④經過語法分析後,tokens會被轉換成AST,中間會經過Parser和PreParser過程:
- Parser:直接解析,將tokens轉成AST樹;
- PreParser:預解析(為什麼需要預解析?)
- 因為並不是所有的JavaScript代碼,在一開始時就會執行的,如果一股腦對所有JavaScript代碼進行解析,必然會影響性能,所以V8就實現了Lazy Parsing(延遲解析)方案,對不必要的函數代碼進行預解析,也就是先解析急需要執行的代碼內容,對函數的全量解析會放到函數被調用時進行。
- ⑤生成AST後,會被Ignition轉換成位元組碼,然後轉成機器碼,最後就是代碼的執行過程了;