新聞中心
V8 引擎是由 Google 用 C++ 開源的 JavaScript 與 WebAssembly 引擎,目前像是 Chrome 和 Node.js 都是使用 V8 在執(zhí)行 JavaScript。除了 V8 以外還有 SpiderMonkey(最早的 JavaScript 引擎,目前是 Firefox 瀏覽器在使用)與 JavaScriptCore(Safari 瀏覽器使用)等其他 JavaScript 引擎。

創(chuàng)新互聯(lián)成都企業(yè)網(wǎng)站建設服務,提供網(wǎng)站設計、成都網(wǎng)站制作網(wǎng)站開發(fā),網(wǎng)站定制,建網(wǎng)站,網(wǎng)站搭建,網(wǎng)站設計,自適應網(wǎng)站建設,網(wǎng)頁設計師打造企業(yè)風格網(wǎng)站,提供周到的售前咨詢和貼心的售后服務。歡迎咨詢做網(wǎng)站需要多少錢:18982081108
好的,那麼 V8 引擎到底是如何執(zhí)行 JavaScript 的呢?
V8 引擎執(zhí)行流程
Scanner
V8 引擎取得 JavaScript 源代碼后的第一步,就是讓 Parser 使用 Scanner? 提供的 Tokens(Tokens 裡有 JavaScript 內(nèi)的語法關(guān)鍵字,像是 function、async、if 等),將 JavaScript 的原始碼解析成** abstract syntax tree**,就是大家常在相關(guān)文章中看到的 AST(抽象語法樹)。
如果好奇 AST 長什麼樣子的話,可以使用 acron 這個 JavaScript Parser,或是 這個網(wǎng)站 生成 AST 參考看看。以下是使用 acron 的代碼:
const { Parser } = require('acorn')
const javascriptCode = `
let name;
name = 'Clark';
`;
const ast = Parser.parse(javascriptCode, { ecmaVersion: 2020 });
console.log(JSON.stringify(ast));下方是解析 let name; name = 'Clark'; 所得到的 AST:
{
"type": "Program",
"start": 0,
"end": 31,
"body": [
{
"type": "VariableDeclaration",
"start": 3,
"end": 12,
"declarations": [
{
"type": "VariableDeclarator",
"start": 7,
"end": 11,
"id": {
"type": "Identifier",
"start": 7,
"end": 11,
"name": "name"
},
"init": null
}
],
"kind": "let"
},
{
"type": "ExpressionStatement",
"start": 15,
"end": 30,
"expression": {
"type": "AssignmentExpression",
"start": 15,
"end": 29,
"operator": "=",
"left": {
"type": "Identifier",
"start": 15,
"end": 19,
"name": "name"
},
"right": {
"type": "Literal",
"start": 22,
"end": 29,
"value": "Clark",
"raw": "'Clark'"
}
}
}
],
"sourceType": "script"
}如果再進一步,將上方的 AST 轉(zhuǎn)化成圖表,會長這樣:
AST 可以從上到下,由左而右去理解它在執(zhí)行的步驟:
- 走 VariableDeclaration 建立名字為name 的變量。
- 走ExpressionStatement 到表達式。
- 走AssignmentExpression? 遇到=?,且左邊為name?,右邊為字串Clark。
產(chǎn)生 AST 后,就完成了 V8 引擎的第一個步驟。
JIT(Just-In-Time)
JIT 的中文名稱是即時編譯,這也是 V8 引擎所採用在執(zhí)行時編譯 JavaScript 的方式。
將代碼轉(zhuǎn)變?yōu)榭蓤?zhí)行的語言有幾種方法,第一種是編譯語言,像是 C/C++ 在寫完代碼的時候,會先經(jīng)過編譯器將代碼變成機器碼才能執(zhí)行。第二種就像 JavaScript,會在執(zhí)行的時候?qū)⒋a解釋成機器懂的語言,一邊解釋邊執(zhí)行的這種,稱作直譯語言。
編譯語言的好處是可以在執(zhí)行前的編譯階段,審視所有的代碼,將可以做的優(yōu)化都完成,但直譯語言就無法做到這一點,因為執(zhí)行時才開始解釋的關(guān)係,執(zhí)行上就相對較慢,也沒辦法在一開始做優(yōu)化,為了處理這個狀況,JIT 出現(xiàn)了。
JIT 結(jié)合解釋和編譯兩者,讓執(zhí)行 JavaScript 的時候,能夠分析代碼執(zhí)行過程的情報,并在取得足夠情報時,將相關(guān)的代碼再編譯成效能更快的機器碼。
聽起來 JIT 超讚,而在 V8 引擎裡負責處理 JIT 的左右手分別為 Ignition 和 **TurboFan**。
Ignition & TurboFan
成功解析出 AST 后,Ignition? 會將 AST? 解釋為 ByteCode?,成為可執(zhí)行的語言,但是 V8 引擎還未在這裡結(jié)束,Ignition 用 ByteCode 執(zhí)行的時候,會搜集代碼在執(zhí)行時的類型信息。舉個例子,如果我們有個 sum? 函式,并且始終確定呼叫的參數(shù)類型都是 number,那麼 Ignition 會將它記錄起來。
此時,在另一方面的 TurboFan 就會去查看這些信息,當它確認到“只有 number? 類型的參數(shù)會被送進 sum? 這個函式執(zhí)行”這個情報的時候,就會進行 Optimization,把 sum 從 ByteCode 再編譯為更快的機器碼執(zhí)行。
如此一來,就能夠保留 JavaScript 直譯語言的特性,又能夠在執(zhí)行的時候優(yōu)化性能。
但畢竟是 JavaScript,誰也不敢保證第一百萬零一次送進來的參數(shù)仍然是 number?,因此當 sum 接收到的參數(shù)與之前 Optimization 的策略不同時,就會進行 Deoptimization 的動作。
TurboFan 的 Optimization 并不是將原有的 ByteCode 直接變成機器碼,而是在產(chǎn)生機器碼的同時,增加一個 Checkpoint 到 ByteCode 和機器碼之間,在執(zhí)行機器碼之前,會先用 Checkpoint 檢查是否與先前 Optimization 的類型符合。這樣的話,當 sum 以與 Optimization 不同的類型被呼叫的時候,就會在 Checkpoint 這關(guān)被擋下來,并進行 Deoptimization。
最后如果 TurboFan 重複執(zhí)行了 5 次 Optimization 和 Deoptimization 的過程,就會直接放棄治療,不會再幫這個函式做 Optimization。
那到底該怎麼知道 TurboFan 有沒有真的做 Optimization 咧?我們可以用下方的代碼來做個實驗:
const loopCount = 10000000;
const sum = (a, b) => a + b;
performance.mark('first_start');
for (let i = 0; i < loopCount; i += 1) {
sum(1, i);
}
performance.mark('first_end');
performance.mark('second_start');
for (let i = 0; i < loopCount; i += 1) {
sum(1, i);
}
performance.mark('second_end');
performance.measure('first_measure', 'first_start', 'first_end');
const first_measures = performance.getEntriesByName('first_measure');
console.log(first_measures[0]);
performance.measure('second_measure', 'second_start', 'second_end');
const second_measures = performance.getEntriesByName('second_measure');
console.log(second_measures[0]);
上方利用 Node.js v18.1 的 perf_hooks 做執(zhí)行速度的測量,執(zhí)行結(jié)果如下:
執(zhí)行后會發(fā)現(xiàn)第一次執(zhí)行的時間花了 8 秒,第二次的執(zhí)行時間只花了 6 秒,大家可以再把 loopCount 的數(shù)字改大一點,差距會越來越明顯。
但是這麼做仍然沒辦法確認是 TurboFan 動了手腳,因此接下來執(zhí)行的時候,加上 --trace-opt 的 flag,看看 Optimization 是否有發(fā)生:
執(zhí)行后的信息顯示了 TurboFan 做的幾次 Optimization,也有把每次 Optimization 的原因?qū)懴聛?,像第一二行分別顯示了原因為 hot and stable 和 small function,這些都是 TurboFan 背后做的 Optimization 策略。
那 Deoptimization 的部分呢?要測試也很簡單,只要把第二個迴圈的參數(shù)型別改成 String 送給 sum 函式執(zhí)行,那 TurboFan 就會進行 Deoptimization,為了查看 Deoptimization 的訊息,下方執(zhí)行的時候再加上 --trace-deopt:
在 highlight 的那一段,就是因為送入 sum 的參數(shù)型別不同,所以執(zhí)行了 Deoptimization,但是接下來又因為一直送 String 進 sum 執(zhí)行,所以 TurboFan 又會再替 sum 重新做 Optimization。
總結(jié)
整理 V8 引擎執(zhí)行 JavaScript 的過程后,能夠得出下方的流程圖:
搭配上圖解說 V8 引擎如何執(zhí)行 JavaScript:
- Parser 透過 Scanner 的 Tokens 將 JavaScript 解析成 AST
- Ignition 把 AST 解釋成 ByteCode 執(zhí)行,并且在執(zhí)行時搜集類型信息
- TurboFan 針對信息將 ByteCode 再編譯為機器碼
- 如果機器碼檢查到這次的執(zhí)行和之前 Optimization 策略不同,就做 Deop timization 回到 ByteCode,以繼續(xù)搜集類型信息或放棄治療。
作者:神Q超人 > 來源:medium
原文:https://medium.com/tarbugs/%E5%9F%B7%E8%A1%8C-javascript-%E7%9A%84-v8-%E5%BC%95%E6%93%8E%E5%81%9A%E4%BA%86%E4%BB%80%E9%BA%BC-f97e5b4b3fbe
作者:Andy Chen
譯者:前端小智
來源:medium
原文:https://medium.com/starbugs/%E5%8E%9F%E4%BE%86%E7%A8%8B%E5%BC%8F%E7%A2%BC%E6%89%93%E5%8C%85%E4%B9%9F%E6%9C%89%E9%80%99%E9%BA%BC%E5%A4%9A%E7%9C%89%E8%A7%92-%E6%B7%BA%E8%AB%87-tree-shaking-%E6%A9%9F%E5%88%B6-8375d35d87b2?
分享名稱:面試寫:說說執(zhí)行JavaScript的V8引擎做了什么?
標題路徑:http://www.dlmjj.cn/article/dhseihj.html


咨詢
建站咨詢
