深入瀏覽器工作原理和JS引擎(V8引擎為例)

MomentYY發表於2022-01-06

瀏覽器工作原理和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,然後再生成目的碼;

  • 參考官方文件:https://v8.dev/blog/scanner

(2)Ignition模組:一個直譯器,可以將AST轉換成ByteCode(位元組碼)。

  • 位元組碼(Byte-code):是一種包含執行程式,由一序列 op 程式碼/資料對組成的二進位制檔案,是一種中間碼。
  • 將JS程式碼轉成AST是便於引擎對其進行操作,前面說到JS程式碼最終是轉成機器碼給CPU執行的,為什麼還要先轉換成位元組碼呢?
    • 因為JS執行所處的環境是不一定的,可能是windows或Linux或iOS,不同的作業系統其CPU所能識別的機器指令也是不一樣的。位元組碼是一種中間碼,本身就有跨平臺的特性,然後V8引擎再根據當前所處的環境將位元組碼編譯成對應的機器指令給當前環境的CPU執行。
  • 參考官方文件:https://v8.dev/blog/ignition-interpreter

(3)TurboFan模組:一個編譯器,可以將位元組碼編譯為CPU認識的機器碼。

  • 在瞭解TurboFan模組之前可以先考慮一個問題,如果每執行一次程式碼,就要先將AST轉成位元組碼然後再解析成機器指令,是不是有點損耗效能呢?強大的V8早就考慮到了,所以出現了TurboFan這麼一個庫;
  • TurboFan可以獲取到Ignition收集的一些資訊,如果一個函式在程式碼中被多次呼叫,那麼就會被標記為熱點函式,然後經過TurboFan轉換成優化的機器碼,再次執行該函式的時候就直接執行該機器碼,提高程式碼的執行效能;
  • 圖中還存在一個Deoptimization過程,其實就是機器碼被還原成ByteCode,比如,在後續執行程式碼的過程中傳入熱點函式的引數型別發生了變化(如果給sum函式傳入number型別的引數,那麼就是做加法;如果給sum函式傳入String型別的引數,那麼就是做字串拼接),可能之前優化的機器碼就不能滿足需求了,就會逆向轉成位元組碼,位元組碼再編譯成正確的機器碼進行執行;
  • 從這裡就可以發現,如果在編寫程式碼時給函式傳遞固定型別的引數,是可以從一定程度上優化我們程式碼執行效率的,所以TypeScript編譯出來的JavaScript程式碼的效能是比較好的;
  • 參考官方文件:https://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轉換成位元組碼,然後轉成機器碼,最後就是程式碼的執行過程了;

相關文章