JavaScript 是如何工做的系列——第二篇
在上一篇《JavaScript 從下載到執行(阻塞、defer、async)》中介紹了瀏覽器是什麼時候開始下載和執行 JavaScript 的 ,以及阻塞 HTML 解析問題。本篇文章將深刻 JavaScript 引擎,瞭解 JavaScript 引擎(V8)是如何執行 JavaScript 代碼的。javascript
咱們都知道 JavaScript 代碼是不能直接在 CPU 中運行的,須要將 JavaScript 代碼翻譯成機器能夠識別的指令(二進制碼)。那麼誰來翻譯呢?沒錯,就是 JavaScript 引擎。固然 JavaScript 引擎並非簡單的將 JavaScript 代碼翻譯成 CPU 可執行的機器代碼,這中間還涉及了不少優化策略。
html
JavaScript 引擎是一個負責整個 JavaScript 程序執行的應用程序,它是瀏覽器引擎的一部分。如今也被應用於 Node.js 中。前端
大多數主流的瀏覽器對於 JavaScript 引擎都有本身的實現:
java
本篇文章將以目前最爲流行的 Chrome 瀏覽器的 V8 引擎爲例,進行介紹。git
下圖展現了 V8 引擎工做的基本流程:
github
其中 Parser(解析器)、Ignition(解釋器)、TurboFan(編譯器) 是 V8 中三個主要的工做模塊,除此以外還有一個主要的工做模塊是 Orinoco(垃圾回收)。編程
工做流程概述:segmentfault
下面咱們將詳細介紹這四個步驟。數組
V8 引擎首先會掃描全部的源代碼,進行詞法分析(詞法分析是經過 Scanner 模塊來完成的,本文不進行詳細介紹)。瀏覽器
什麼是詞法分析?
詞法分析(Tokenizing/Lexing)就是將程序源代碼分解成對編程語言來講有意義的代碼塊,這些代碼塊被稱爲詞法單元(token)。
咱們來看下 var a = 2;
這句代碼通過詞法分析後會被分解出哪些 tokens ?
從上圖中能夠看到,這句代碼最終被分解出了五個詞法單元:
var
關鍵字a
標識符=
運算符2
數值 ;
分號Tokens 在線查看網站: https://esprima.org/demo/pars...
Parser 是 V8 的解析器,負責根據生成的 Tokens 進行語法分析。Parser 的主要工做包括:
詞法做用域相關內容,敬請期待文章《JavaScript 之做用域》
咱們簡單介紹下什麼是抽象語法樹(Abstract Syntax Tree, AST)?
仍是上面的例子,咱們來看下 var a = 2;
通過語法分析後生成的AST是什麼樣子的:
能夠看到這段程序的類型是 VariableDeclaration,也就是說這段代碼是用來聲明變量的。
AST 在線查看網站: https://astexplorer.net/
什麼是預解析 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 。
咱們仍是以示例來講明:
function foo() { console.log('a'); function inline() { console.log(''b) } } (function bar() { console.log('c') })(); foo();
(function bar() {console.log(c)})()
時,它會知道這是一個當即執行表達式(IIFE),會當即被執行,因此會使用 Parser 對其解析。Ignition 是 V8 的解釋器,它負責的工做包括:
什麼是字節碼?
字節碼(Bytecode)是一種中間碼,它比機器碼更抽象,也更輕量,須要直譯器轉譯後才能成爲機器碼的中間代碼。
早期版本的 V8 ,並無生成中間字節碼的過程,而是將全部源碼轉換爲了機器代碼。機器代碼雖然執行速度更快,可是佔用內存大。
TurboFan 是 V8 的優化編譯器,負責將字節碼和一些分析數據做爲輸入並生成優化的機器代碼。
上面咱們說到,當 Ignition 將 JavaScript 代碼轉換爲字節碼後,程序就能夠執行了,那麼 TurboFan 還有什麼用呢?
咱們再來看下 V8 的工做流程圖:
咱們主要關注 Ignition 和 TurboFan 的交互:
當 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 是 V8 的垃圾回收模塊(garbage collector),負責將程序再也不須要的內存空間回收;
本篇文章主要介紹了 JavaScript 引擎(V8)執行 JavaScript 代碼的工做流程,主要涉及了Parser、Ignition、TurboFan、 Orinoco 四個模塊。下篇文章將開始講解 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