JavaScript 引擎(V8)是如何工做的

JavaScript 是如何工做的系列——第二篇

前言

在上一篇《JavaScript 從下載到執行(阻塞、defer、async)》中介紹了瀏覽器是什麼時候開始下載和執行 JavaScript 的 ,以及阻塞 HTML 解析問題。本篇文章將深刻 JavaScript 引擎,瞭解 JavaScript 引擎(V8)是如何執行 JavaScript 代碼的。javascript

什麼是 JavaScript 引擎

咱們都知道 JavaScript 代碼是不能直接在 CPU 中運行的,須要將 JavaScript 代碼翻譯成機器能夠識別的指令(二進制碼)。那麼誰來翻譯呢?沒錯,就是 JavaScript 引擎。固然 JavaScript 引擎並非簡單的將 JavaScript 代碼翻譯成 CPU 可執行的機器代碼,這中間還涉及了不少優化策略。
Image  7html

JavaScript 引擎是一個負責整個 JavaScript 程序執行的應用程序,它是瀏覽器引擎的一部分。如今也被應用於 Node.js 中。前端

大多數主流的瀏覽器對於 JavaScript 引擎都有本身的實現:
Image  8java

本篇文章將以目前最爲流行的 Chrome 瀏覽器的 V8 引擎爲例,進行介紹。git

V8 引擎

下圖展現了 V8 引擎工做的基本流程:
Image  9github

其中 Parser(解析器)、Ignition(解釋器)、TurboFan(編譯器) 是 V8 中三個主要的工做模塊,除此以外還有一個主要的工做模塊是 Orinoco(垃圾回收)。編程

工做流程概述:segmentfault

  • 首先 V8 引擎會掃描全部的源代碼,進行詞法分析,生成 Tokens;

Image  10

  • Parser 解析器根據 Tokens 生成 AST;
  • Ignition 解釋器將 AST 轉換爲字節碼,並解釋執行;
  • TurboFan 編譯器負責將熱點函數優化編譯爲機器指令執行;

下面咱們將詳細介紹這四個步驟。數組

詞法分析

V8 引擎首先會掃描全部的源代碼,進行詞法分析(詞法分析是經過 Scanner 模塊來完成的,本文不進行詳細介紹)。瀏覽器

什麼是詞法分析?

詞法分析(Tokenizing/Lexing)就是將程序源代碼分解成對編程語言來講有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)

咱們來看下 var a = 2; 這句代碼通過詞法分析後會被分解出哪些 tokens ?
Image  11

從上圖中能夠看到,這句代碼最終被分解出了五個詞法單元:

  • var 關鍵字
  • a 標識符
  • = 運算符
  • 2 數值
  • 分號
Tokens 在線查看網站: https://esprima.org/demo/pars...

語法分析

Parser

Parser 是 V8 的解析器,負責根據生成的 Tokens 進行語法分析。Parser 的主要工做包括:

  • 分析語法錯誤:遇到錯誤的語法會拋出異常;
  • 輸出 AST:將詞法分析輸出的詞法單元流(數組)轉換爲一個由元素逐級嵌套所組成的表明了程序語法結構的樹——抽象語法樹(Abstract Syntax Tree, AST);
  • 肯定詞法做用域

詞法做用域相關內容,敬請期待文章《JavaScript 之做用域》

咱們簡單介紹下什麼是抽象語法樹(Abstract Syntax Tree, AST)?

仍是上面的例子,咱們來看下 var a = 2; 通過語法分析後生成的AST是什麼樣子的:
Image  12

Image  13
能夠看到這段程序的類型是 VariableDeclaration,也就是說這段代碼是用來聲明變量的。

AST 在線查看網站: https://astexplorer.net/

Pre-Parser

什麼是預解析 Pre-Parser?

咱們先來看看下面這段代碼:

function foo () {
    console.log('I\'m function foo')
}

function bar () {
    console.log('I\'m function bar')
}

foo()

上面這段代碼中,若是使用 Parser 解析後,會生成 foo 函數 和 bar 函數的 AST。然而 bar 函數並無被調用,因此生成 bar 函數的 AST 其實是沒有任何意義且浪費時間的。那麼有沒有辦法解決呢?此時就用到了 Pre-Parser 技術。

在 V8 中有兩個解析器用於解析 JavaScript 代碼,分別是 Parser 和 Pre-Parser 。

  • Parser 解析器又稱爲 full parser(全量解析) 或者 eager parser(飢餓解析)。它會解析全部當即執行的代碼,包括語法檢查,生成 AST,以及肯定詞法做用域。
  • Pre-Parser 又稱爲惰性解析,它只解析未被當即執行的代碼(如函數),不生成 AST ,只肯定做用域,以此來提升性能。當預解析後的代碼開始執行時,才進行 Parser 解析。

咱們仍是以示例來講明:

function foo() {
    console.log('a');
    function inline() {
        console.log(''b)
    }
}

(function bar() {
    console.log('c')
})();

foo();
  1. 當 V8 引擎遇到 foo 函數聲明時,發現它未被當即執行,就會採用 Pre-Parser 對其進行解析(inline 函數同)。
  2. 當 V8 遇到(function bar() {console.log(c)})()時,它會知道這是一個當即執行表達式(IIFE),會當即被執行,因此會使用 Parser 對其解析。
  3. 當 foo 函數被調用時,會使用 Parser 對 foo 函數進行解析,此時會對 inline 函數再進行一次預解析,也就是說 inline 函數被預解析了兩次。若是嵌套層級較深,那麼內層的函數會被預解析屢次,因此在寫代碼時,儘量避免嵌套多層函數,會影響性能。

Ignition

Ignition 是 V8 的解釋器,它負責的工做包括:

  • 將 AST 轉換爲中間代碼(字節碼 Bytecode)
  • 逐行解釋執行字節碼:在該階段,就已經開始執行 JavaScript 代碼了。

什麼是字節碼?

字節碼(Bytecode)是一種中間碼,它比機器碼更抽象,也更輕量,須要直譯器轉譯後才能成爲機器碼的中間代碼。

Image  14

早期版本的 V8 ,並無生成中間字節碼的過程,而是將全部源碼轉換爲了機器代碼。機器代碼雖然執行速度更快,可是佔用內存大。

TurboFan

TurboFan 是 V8 的優化編譯器,負責將字節碼和一些分析數據做爲輸入並生成優化的機器代碼。

上面咱們說到,當 Ignition 將 JavaScript 代碼轉換爲字節碼後,程序就能夠執行了,那麼 TurboFan 還有什麼用呢?

咱們再來看下 V8 的工做流程圖:
Image  15

咱們主要關注 Ignition 和 TurboFan 的交互:
Image  16

當 Ignition 開始執行 JavaScript 代碼後,V8 會一直觀察 JavaScript 代碼的執行狀況,並記錄執行信息,如每一個函數的執行次數、每次調用函數時,傳遞的參數類型等。

若是一個函數被調用的次數超過了內設的閾值,監視器就會將當前函數標記爲熱點函數(Hot Function),並將該函數的字節碼以及執行的相關信息發送給 TurboFan。TurboFan 會根據執行信息作出一些進一步優化此代碼的假設,在假設的基礎上將字節碼編譯爲優化的機器代碼。若是假設成立,那麼當下一次調用該函數時,就會執行優化編譯後的機器代碼,以提升代碼的執行性能。

那若是假設不成立呢?不知道大家有沒有注意到上圖中有一條由 optimized code 指向 bytecode 的紅色指向線。此過程叫作 deoptimize(優化回退),將優化編譯後的機器代碼還原爲字節碼。

讀到這裏,你可能有些疑惑:這個假設是什麼假設呢?以及爲何要優化回退?咱們來看下面的例子。

function sum (a, b) {
    return a + b;
}

咱們都知道 JavaScript 是基於動態類型的,a 和 b 能夠是任意類型數據,當執行 sum 函數時,Ignition 解釋器會檢查 a 和 b 的數據類型,並相應地執行加法或者鏈接字符串的操做。

若是 sum 函數被調用屢次,每次執行時都要檢查參數的數據類型是很浪費時間的。此時 TurboFan 就出場了。它會分析監視器收集的信息,若是之前每次調用 sum 函數時傳遞的參數類型都是數字,那麼 TurboFan 就預設 sum 的參數類型是數字類型,而後將其編譯爲機器指令。

可是當某一次的調用傳入的參數再也不是數字時,表示 TurboFan 的假設是錯誤的,此時優化編譯生成的機器代碼就不能再使用了,因而就須要進行優化回退。

Orinoco

Orinoco 是 V8 的垃圾回收模塊(garbage collector),負責將程序再也不須要的內存空間回收;

下一篇

本篇文章主要介紹了 JavaScript 引擎(V8)執行 JavaScript 代碼的工做流程,主要涉及了Parser、Ignition、TurboFan、 Orinoco 四個模塊。下篇文章將開始講解 JavaScript 執行機制中的核心概念——執行上下文。

傳送門《JavaScript 之執行上下文》

參考:

The Journey of JavaScript: from Downloading Scripts to Execution - Part
The Journey of JavaScript: from Downloading Scripts to Execution - Part II
視野前端(二)V8引擎是如何工做的
What is V8?
JavaScript 引擎 V8 執行流程概述
Ignition: An Interpreter for V8 [BlinkOn]
How JavaScript works: an overview of the engine, the runtime, and the call stack
相關文章
相關標籤/搜索