天天看點

視野前端(二)V8引擎是如何工作的

視野前端(二)V8引擎是如何工作的

許多同學在閱讀了基礎進階系列文章之後,對JS代碼的執行順序了解得更清晰了。可也有不少好學的大佬在此基礎上進一步思考,JS引擎到底是如何工作的?什麼時候解析?什麼時候執行?特别是在其他地方閱讀了不少各種說法的文章之後,疑惑更重了。

這裡就以V8引擎為例,跟大家聊一聊,JS引擎是如何工作的。

JS引擎是一個應用程式,它是浏覽器引擎的一部分。每個浏覽器的JS引擎都不一樣。例如chrome的V8,firefox的SpiderMonkey,Safari的Nitro等等。

所有的JS引擎原則上都會按照ECMAScript标準來實作。是以大家的實作方式可能有所差異,解析原理也不盡相同,但大體表現基本上能保持一緻。想要了解JS引擎的工作思路,了解V8就足夠了。

視野前端(二)V8引擎是如何工作的

Chrome(還有Nodejs)的JS引擎是V8,他的内部有許多小的子子產品組成。這裡我們隻需要了解其中最常用的四個子產品即可。

1.parser

顧名思義。這個子產品的作用是将我們自己編寫的JS源碼,轉換為抽象文法樹(Abstract Syntax Tree)。在許多其他文章裡,提到的詞法文法分析過程,就是

parser

來完成。

我們可以通過線上網站 https://esprima.org/demo/parse.html# 來觀察我們的代碼通過詞法分析變成AST之後大概會是神馬樣子。

視野前端(二)V8引擎是如何工作的

從該工具中,我們還發現一個在介紹詞法分析過程的文章裡經常提到的一個東西: Token

token: 詞義機關,是指文法上不能再分割的最小機關,可能是單個字元,也可能是一個字元串。

工具中使用如下的方式來表示多個tokens

[
  {
    "type": "Keyword",
    "value": "var"
  },
  {
    "type": "Identifier",
    "value": "a"
  },
  {
    "type": "Punctuator",
    "value": "="
  },
  {
    "type": "Numeric",
    "value": "10"
  },
  {
    "type": "Punctuator",
    "value": ","
  },
  {
    "type": "Identifier",
    "value": "b"
  },
  {
    "type": "Punctuator",
    "value": "="
  },
  {
    "type": "Numeric",
    "value": "20"
  }
]           

複制

那麼,parser子產品的工作過程,就比較明了了。大緻如下:

視野前端(二)V8引擎是如何工作的
此圖僅為大緻過程,例如官方文檔中提到的,tokens的過程具體是由一個名為scanner的掃描工具來完成。
視野前端(二)V8引擎是如何工作的

我們知道,聲明多個連續的變量時,可以隻使用一個關鍵字,如下:

var a = 10, b = 20           

複制

這種方式比多個變量各自聲明性能上會更好一點,為什麼?

利用工具,觀察一下兩種方式下tokens和AST的不同,就能馬上明白了。

那麼問題來了,在這個過程中,執行上下文建立了沒有?

其實還沒有,我們的代碼在這個階段,還沒有正式進入運作。

是以留一個簡單的問題,如下的代碼,直接執行,在這個階段會直接報錯嗎?

如果有興趣,在評論裡留下你的答案與分析。

var a = b;           

複制

1.Ignition

在v8文檔中可以得知,Ignition是V8提供的一個解釋器。他的作用是負責将抽象文法樹AST轉換為位元組碼。并同時收集下一個階段(編譯)所需要的資訊。這個過程,我們也可以了解為預編譯過程。

在之前我對變量對象的介紹中,曾經用下面的方式表達執行上下文的生命周期。這裡預編譯過程,其實就是執行上下文的第一個階段。如圖所示:

視野前端(二)V8引擎是如何工作的
因為基于性能優化的考慮,預編譯過程與真正的編譯有的時候不會區分的那麼明确,有的代碼在預編譯階段就能直接執行。

1.TurboFan

V8引擎的編譯器子產品。利用Ignition收集到的資訊,将位元組碼轉換為彙編代碼。

這也是我們之前提到過的可執行代碼的執行階段。

當然,到這裡,如果不是對V8特别感興趣的話,就不必在繼續深究具體的細節了。基本上JS代碼的執行過程都相對清晰。

官方文檔中,我們可以查閱一個講述V8引擎優化過程[1]的一個PPT,可以發現,在不同的版本中,解釋器與編譯器的互動過程每個版本都在變化。

視野前端(二)V8引擎是如何工作的
視野前端(二)V8引擎是如何工作的
視野前端(二)V8引擎是如何工作的
視野前端(二)V8引擎是如何工作的
視野前端(二)V8引擎是如何工作的
視野前端(二)V8引擎是如何工作的

這裡截取了一些圖展示編譯過程的演變,PPT裡面還有很多更詳細的介紹,如果感興趣的同學可以閱讀PPT做更深入的了解。

為了達到更好的性能,執行過程并非嚴格按照先由解釋器解析,然後交給編譯器編譯的定式執行。JS作為解釋型的動态語言,在整個解析編譯的過程中,就有許多優化的空間。例如我們常常聽到的JIT模式。

我們自己也能夠猜到一些優化的點:

例如,如果一個函數不被調用,我們可以不用去編譯它。

一個函數被調用很多次,那麼我們可以想辦法給他标記上,隻需要編譯一次等等。

1.Orinoco

垃圾回收子產品。

Orinoco也是使用我們熟知的标記清除法來進行垃圾回收。

當執行上下文建立時,變量進入該環境,我們就可以對該變量對應的記憶體進行标記。如果執行上下文執行完畢,這個時候,就可以将所有進入該環境的變量标記為可清除狀态。我們通俗的說法就是,當一份記憶體失去了引用,那麼它就會被垃圾回收工具回收。

不過還有兩個需要注意的地方。

一個是全局上下文。在程式結束之前,全局上下文始終存在。通常來說,JS程式運作期間,全局上下文不會有執行結束的時間節點。是以定義在全局上下文的狀态永遠都不會被标記。除非我們手動将變量設定為null,它對應的記憶體都不會被回收。

另外一個是閉包。因為閉包的特性是能夠始終保持記憶體的引用。是以當我們希望利用閉包的特性達到某些目的時,即使它對應的執行上下文已經執行完畢了,我們也會想辦法讓記憶體的引用始終保持。

References

[1]

V8引擎優化過程: https://docs.google.com/presentation/d/1chhN90uB8yPaIhx_h2M3lPyxPgdPmkADqSNAoXYQiVE/edit#slide=id.g1357e6d1a4_0_58