JS引擎執行一段js的過程

最初,JavaScript 只能在 Web 瀏覽器中運行,但是隨着 Node 的出現,現在 JavaScript 也可以在服務端運行。雖然我們可能知道應該在何時何地去使用它, 但是我們真的瞭解這些腳本執行的背後發生了什麼嗎?

JavaScript 是一門高級語言,但是最終計算機能理解只有1和0。那麼我們編寫的代碼是如何被計算機理解的呢?掌握所學編程語言的基礎知識將讓您能編寫出更好的代碼。在本文中,我們僅探討一個問題:JS引擎是如何工作的?

一、JavaScript 引擎

這是本文將要探索的主要內容,它負責使計算機理解我們編寫的 JS 代碼。JavaScript 引擎是一種用於將我們的代碼轉換爲機器可讀語言的引擎。如果沒有 JavaScript 引擎,您編寫的代碼對計算機來說簡直是一堆“胡言亂語”。不僅僅是 JavaScript ,其他所有編程語言都需要一個類似的引擎,來將這些“胡言亂語”轉換成對計算機有意義的語言。

接下來,我們將深入研究這些引擎,以瞭解它們是如何翻譯 JavaScript 文件的。

二、JavaScript 引擎的內裏

JavaScript 引擎只是分析我們的代碼並將其翻譯成另一種語言的工具。V8 是最受歡迎的 JavaScript 引擎之一,也是 Chrome 和 NodeJS 使用的引擎。它是用 C++(一種底層語言)編寫的。但是如果每個人都創造一個引擎,那場面就不是可控範圍內的了。

因此,爲了給這些引擎確立一個規範,ECMA 的標準誕生了,該標準主要提供如何編寫引擎和 JavaScript 所有功能的規範。這就是新功能能在 ECMAScript 6、7、8 上實現的原因。同時,引擎也進行了更新以支持這些新功能。於是,我們便可以在開發過程中檢查了瀏覽器中 JS 高級功能的可用性。

下面我們對 V8 引擎進行進一步的探索,因爲基本概念在所有引擎中是一致的。

JavaScript V8 Engine

 

上圖就是 JS Engine 內部的工作流程。我們輸入的代碼將通過以下階段,

  • Parser

  • AST

  • Interpreter 生成 ByteCode

  • Profiler

  • Compiler 生成優化後的代碼

別被上面的流程給唬住了,在幾分鐘後您將瞭解它們是協同運作的。

在進一步深入這些階段之前,您需要先了解 Interpreter 和 Compiler 的區別。

三、Interpreter VS Compiler

通常,將代碼轉換成機器可讀語言的方法有兩種。我們將要討論的概念不僅適用於 JavaScript ,而且適用於大多數編程語言,例如 Python,Java 等。

  • Interpreter 逐行讀取代碼並立即執行。

  • Compiler 讀取您的整個代碼,進行一些優化,然後生成優化後的代碼。

讓我們來看下面這個例子。

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

for (let i = 0; i < 1000; i++) {
  add(1 + 1);
}

上面的示例循環調用了 add 函數1000次,該函數將兩個數字相加並返回總和。

Interpreter 接收上面的代碼後,它將逐行讀取並立即執行代碼,直到循環結束。它的工作僅僅是實時地將代碼轉換爲我們的計算機可以理解的內容。

如果這段代碼接受者是 Compiler,它會先完整地讀取整個程序,對我們要執行的代碼進行分析,並生成電腦可以讀懂的機器語言。過程如同獲取 X(我們的JS文件)並生成 Y(機器語言)一樣。如果我們使用 Interpreter 執行 Y,則會獲得與執行 X 相同的結果。

從上圖中可以看出,ByteCode 只是中間碼,計算機仍需要對其進行翻譯才能執行。但是 Interpreter 和 Compiler 都將源代碼轉換爲機器語言,它們唯一的區別在於轉換的過程不盡相同。

  • Interpreter 逐行將源代碼轉換爲等效的機器代碼。

  • Compiler 在一開始就將所有源代碼轉換爲機器代碼。

如果你想了解更多它們之前的區別,推薦閱讀這篇文章。

當您閱讀完上面的推薦文章後,您可能已經瞭解到 Babel 實際上是一個 JS Compiler ,它可以接收您編寫的新版本 JS 代碼並向下編譯爲與瀏覽器兼容的 JS 代碼(舊版本的 JS 代碼)。

Interpreter 和 Compiler 的優缺點

Interpreter 的優點是無需等待編譯即可立即執行代碼。這對在瀏覽器中運行 JS 提供了極大的便利,因爲所有用戶都不想浪費時間在等待代碼編譯這件事上。但是,當有大量的 JS 代碼需要執行時會運行地比較慢。還記得上面例子中的那一小段代碼嗎?代碼中執行了1000次函數調用。函數 add 被調用了1000次,但他的輸出保持不變。但是 Interpreter 還是逐行執行,會顯得比較慢。

在同樣的情況下,Compiler 可以通過用2代替循環(因爲 add 函數每次都是執行1 + 1)來進行一些優化。Compiler 最終給出的優化代碼可以在更短的時間內執行完成。

綜上所述,Interpreter 可以立即開始執行代碼,但不會進行優化。Compiler 雖然需要花費一些時間來編譯代碼,但是會生成對執行時更優的代碼。

好的,Interpreter 和 Compiler 必要知識我們已經瞭解了。現在讓我們回到主題——JS 引擎。

因此,考慮到編譯器和解釋器的優缺點,如果我們同時利用兩者的優點,該怎麼辦?這就是 JIT(Just In Time) Compiler 的用武之地。它是 Interpreter 和 Compiler 的結合,現在大多數瀏覽器都在更快,更高效地實現此功能。同時 V8 引擎也使用此功能(如第一張圖)。

在這個過程中,Parser 是一種通過各種 JavaScript 關鍵字來識別,分析和分類程序各個部分的解析器。它可以區分代碼是一個方法還是一個變量。

然後,AST(抽象語法樹) 基於 Parser 的分類構造樹狀結構。您可以使用 AST Explorer 查看該樹的結構。

隨後將 AST 提供給 Interpreter 生成 ByteCode。如上文所述,ByteCode 不是最底層的代碼,但可以被執行。在此階段,瀏覽器藉助 V8 引擎執行 ByteCode 進行工作,因此用戶無需等待。

同時,Profiler 將查找可以被優化的代碼,然後將它們傳遞給 Compiler。Compiler 生成優化代碼的同時,瀏覽器暫時用 ByteCode 執行操作。並且,一旦 Compiler 生成了優化代碼,優化代碼則將完全替換掉臨時的 ByteCode。

通過這種方式,我們可以充分利用 Interpreter 和 Compiler 的優點。Interpreter 執行代碼的同時,Profiler 尋找可以被優化的代碼,Compiler 則創建優化的代碼。然後,將 ByteCode 碼替換爲優化後的較爲底層的代碼,例如機器代碼。

這僅意味着性能將在逐漸提高,同時不會有阻塞執行的時間。

關於 ByteCode

作爲機器代碼,ByteCode 不能被所有計算機理解及執行。它仍然需要像虛擬機或像 Javascript V8 引擎這樣的中間件才能將其轉換爲機器可讀的語言。這就是爲什麼我們的瀏覽器可以在上述5個階段中藉助 JavaScript 引擎在 Interpreter 中執行 ByteCode 的原因。

所以您可以會有另一個問題,

JavaScript 是一門解釋型語言嗎?

JavaScript 是但不完全是一門解釋型語言。Brendan Eich 最初是在 JavaScript 的早期階段創建 JavaScript 引擎 “ SpiderMonkey” 的。該引擎有一個 Interpreter 來告訴瀏覽器該怎麼執行代碼。但是現在我們的引擎不僅包括了 Interpreter,還有 Compiler。我們的代碼不僅可以被轉換成 ByteCode,還可以被編譯輸出優化後的代碼。因此,從技術上講,這完全取決於引擎是如何實現的。

JavaScript 引擎的整體工作原理就是這樣。相信您無需學習 JavaScript 也可以理解。當然,您甚至可以在不知道 JavaScript 如何工作的情況下編寫代碼。但是,如果我們瞭解一些幕後的知識,或許能讓我們編寫出更好的代碼。

四、V8 執行一段JS代碼的過程

站在 V8 的角度,理解其中的執行機制,能夠幫助我們理解很多的上層應用,包括Babel、Eslint、前端框架的底層機制。那麼,一段 JavaScript 代碼放在 V8 當中究竟是如何執行的呢?

JS屬於解釋型語言,對於解釋型的語言說,解釋器會對源代碼做如下分析:

  • 通過詞法分析和語法分析生成 AST(抽象語法樹)
  • 生成字節碼

然後解釋器根據字節碼來執行程序。但 JS 整個執行的過程其實會比這個更加複雜,接下來就來一一地拆解。

1.生成 AST

生成 AST 分爲兩步——詞法分析和語法分析。

詞法分析即分詞,它的工作就是將一行行的代碼分解成一個個token。 比如下面一行代碼:

let name = 'sanyuan'

其中會把句子分解成四個部分:

 

即解析成了四個token,這就是詞法分析的作用。

接下來語法分析階段,將生成的這些 token 數據,根據一定的語法規則轉化爲AST。舉個例子:

let name = 'sanyuan'
console.log(name)

最後生成的 AST 是這樣的:

 

當生成了 AST 之後,編譯器/解釋器後續的工作都要依靠 AST 而不是源代碼。順便補充一句,babel 的工作原理就是將 ES6 的代碼解析生成ES6的AST,然後將 ES6 的 AST 轉換爲 ES5 的AST,最後纔將 ES5 的 AST 轉化爲具體的 ES5 代碼。由於本文着重闡述原理,關於 babel 編譯的細節就不展開了,推薦大家去讀一讀荒山的babel文章, 幫你打開新世界的大門: )

回到 V8 本身,生成 AST 後,接下來會生成執行上下文

2. 生成字節碼

開頭就已經提到過了,生成 AST 之後,直接通過 V8 的解釋器(也叫Ignition)來生成字節碼。但是字節碼並不能讓機器直接運行,那你可能就會說了,不能執行還轉成字節碼幹嘛,直接把 AST 轉換成機器碼不就得了,讓機器直接執行。確實,在 V8 的早期是這麼做的,但後來因爲機器碼的體積太大,引發了嚴重的內存佔用問題。

給一張對比圖讓大家直觀地感受以下三者代碼量的差異:

 

 

很容易得出,字節碼是比機器碼輕量得多的代碼。那 V8 爲什麼要使用字節碼,字節碼到底是個什麼東西?

字節碼是介於AST 和 機器碼之間的一種代碼,但是與特定類型的機器碼無關,字節碼需要通過解釋器將其轉換爲機器碼然後執行。

字節碼仍然需要轉換爲機器碼,但和原來不同的是,現在不用一次性將全部的字節碼都轉換成機器碼,而是通過解釋器來逐行執行字節碼,省去了生成二進制文件的操作,這樣就大大降低了內存的壓力。

3. 執行代碼

接下來,就進入到字節碼解釋執行的階段啦!

在執行字節碼的過程中,如果發現某一部分代碼重複出現,那麼 V8 將它記做熱點代碼(HotSpot),然後將這些代碼編譯成機器碼保存起來,這個用來編譯的工具就是V8的編譯器(也叫做TurboFan) , 因此在這樣的機制下,代碼執行的時間越久,那麼執行效率會越來越高,因爲有越來越多的字節碼被標記爲熱點代碼,遇到它們時直接執行相應的機器碼,不用再次將轉換爲機器碼。

其實當你聽到有人說 JS 就是一門解釋器語言的時候,其實這個說法是有問題的。因爲字節碼不僅配合瞭解釋器,而且還和編譯器打交道,所以 JS 並不是完全的解釋型語言。而編譯器和解釋器的 根本區別在於前者會編譯生成二進制文件但後者不會。

並且,這種字節碼跟編譯器和解釋器結合的技術,我們稱之爲即時編譯(JIT)

這就是 V8 中執行一段JS代碼的整個過程,梳理一下:

  1. 首先通過詞法分析和語法分析生成 AST
  2. 將 AST 轉換爲字節碼
  3. 由解釋器逐行執行字節碼,遇到熱點代碼啓動編譯器進行編譯,生成對應的機器碼, 以優化執行效率

五、同類型文章

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章