- 原文地址:alligator.io/js/v8-engin…
- 翻譯:馬雪琴
V8 是谷歌用於編譯 JavaScript 的引擎,Firefox 一樣也有一個,叫 SpiderMonkey,它和 V8 有一些不一樣,但整體頗爲類似。咱們將在本篇文章中討論 V8。git
V8 引擎的一些基礎點:github
當咱們把壓縮、混淆以及作了各類處理的 JavaScript 放到 V8 引擎中解析時,到底發生了些什麼?瀏覽器
下圖闡述了整個流程,接下來咱們會對其中的每一個步驟進行詳細說明: bash
在本篇文章中,咱們將探討 JavaScript 代碼是如何被解析的,以及如何最大程度的優化 JavaScript 的編譯效率。V8 裏的優化編譯器(又名 Turbofan)拿到 JavaScript 代碼以後,會將其轉化成高效率的機器碼,所以,咱們能向其輸入越多的代碼,咱們的應用就會越快。附註一點,Chrome 裏的解釋器稱做 Ignition。markdown
整個過程當中的第一步是解析 JavaScript。首先探討什麼是解析。ide
解析有兩個階段:函數
哪種方式更好則須要根據實際狀況來決定。工具
下面來看一段代碼。oop
// 變量聲明會被當即解析 const a = 1; const b = 2; // 目前不須要的暫時不解析 function add(a, b) { return a + b; } // add 方法被執行到了,因此須要返回解析該方法 add(a, b); 複製代碼
變量聲明會被當即解析,函數則會被懶解析,但上述代碼裏緊接着就執行了 add(a, b),說明 add 方法是立刻就須要用到的,因此這種狀況下,把 add 函數進行即時解析會更高效。性能
爲了讓 add 方法被當即解析,咱們能夠這樣作:
// 變量聲明會被當即解析 const a = 1; const b = 2; // eager parse this too var add = (function(a, b) { return a + b; })(); // add 方法已經被解析過了,因此這段代碼能夠當即執行 add(a, b); 複製代碼
這就是大多數模塊被建立的過程。那麼,當即解析會是高效 JavaScript 應用的最好方式嗎?
咱們能夠用 optimize-js 這個工具對公共庫代碼進行徹底的當即解析處理,好比對比較有名的 lodash 進行處理後,優化效果是很顯著的:
必須聲明的是,該結果是在 Chrome 瀏覽器中獲得的,其它環境的結果則沒法保證:
若是您須要優化應用,必須在全部的環境中進行測試。
另外一個解析相關的建議是不要讓函數嵌套:
// 糟糕的方式 function sumOfSquares(a, b) { // 這裏將被反覆懶解析 function square(num) { return num * num; } return square(a) + square(b); } 複製代碼
改進後的方式以下:
function square(num) { return num * num; } // 好的方式 function sumOfSquares(a, b) { return square(a) + square(b); } sumOfSquares(a, b); 複製代碼
上述示例中,square 方法只被懶解析了一次。
Chrome 有時候會重寫 JavaScript 代碼,內聯函數便是這樣一種狀況。
下面是一個代碼示例:
const square = (x) => { return x * x } const callFunction100Times = (func) => { for(let i = 100; i < 100; i++) { // func 參數會被調用100次 func(2) } } callFunction100Times(square) 複製代碼
上述代碼會被 V8 引擎進行以下優化:
const square = (x) => { return x * x } const callFunction100Times = (func) => { for(let i = 100; i < 100; i++) { // 函數被內聯後就不會被持續調用了 return x * x } } callFunction100Times(square) 複製代碼
從上面能夠看出,V8 實際上會把 square 函數體內聯,以消除調用函數的步驟。這對提升代碼的性能是頗有用處的。
上述方法存在一點問題,讓咱們看看下面這段代碼:
const square = (x) => { return x * x } const cube = (x) => { return x * x * x } const callFunction100Times = (func) => { for(let i = 100; i < 100; i++) { // 函數被內聯後就不會被持續調用了 func(2) } } callFunction100Times(square) callFunction100Times(cube) 複製代碼
上面的代碼中先會調用 square 函數100次,緊接着又會調用 cube 函數100次。在調用 cube 以前,咱們必須先對 callFunction100Times 進行反優化,由於咱們已經內聯了 square 函數。在這個例子中,square 函數彷佛會比 cube 函數快,但實際上,由於反優化的這個步驟,使得整個執行過程變得更長了。
談到對象,V8 引擎底層有個類型系統能夠區分它們:
對象具備相同的鍵,這些鍵沒有區別。
// 單態示例 const person = { name: 'John' } const person2 = { name: 'Paul' } 複製代碼
對象有類似的結構,並存在一些細微的差異。
// 多態示例 const person = { name: 'John' } const person2 = { name: 'Paul', age: 27 } 複製代碼
這兩個對象徹底不一樣,不能比較。
// 複雜態示例 const person = { name: 'John' } const building = { rooms: ['cafe', 'meeting room A', 'meeting room B'], doors: 27 } 複製代碼
如今咱們瞭解了 V8 裏的不一樣對象,接下來看看 V8 引擎是如何優化對象的。
隱藏類是 V8 區分對象的方式。
讓咱們將這個過程分解一下。
首先聲明一個對象:
const obj = { name: 'John'} 複製代碼
V8 會爲這個對象聲明一個 classId。
const objClassId = ['name', 1] 複製代碼
而後對象會按以下方式被建立:
const obj = {...objClassId, 'John'} 複製代碼
而後當咱們獲取對象裏的 name 屬性時:
obj.name
複製代碼
V8 會作以下查找:
obj[getProp(obj[0], name)]
複製代碼
這就是 V8 建立對象的過程,接下來看看如何優化對象以及重用 classId。
應該儘可能將屬性放在構造器中聲明,以保證對象的結構不變,從而讓 V8 能夠優化對象。
class Point {
constructor(x,y) {
this.x = x
this.y = y
}
}
const p1 = new Point(11, 22) // 隱藏的 classId 被建立
const p2 = new Point(33, 44)
複製代碼
應該保證屬性的順序不變,以下面這個示例:
const obj = { a: 1 } // 隱藏的 classId 被建立
obj.b = 3
const obj2 = { b: 3 } // 另外一個隱藏的 classId 被建立
obj2.a = 1
// 這樣會更好
const obj = { a: 1 } // 隱藏的 classId 被建立
obj.b = 3
const obj2 = { a: 1 } // 隱藏類被複用
obj2.b = 3
複製代碼
接下來咱們看一下其它的 JavaScript 代碼優化建議。
當參數被傳進函數中時,保證參數的類型一致是很重要的。若是參數的類型不一樣,Turbofan 在嘗試優化4次以後就會放棄。
下面是一個例子:
function add(x,y) { return x + y } add(1,2) // 單態 add('a', 'b') // 多態 add(true, false) add({},{}) add([],[]) // 複雜態 - 在這個階段, 已經嘗試了4+次, 不會再作優化了 複製代碼
另外一個建議是保證在全局做用域下聲明類:
// 不要這樣作 function createPoint(x, y) { class Point { constructor(x,y) { this.x = x this.y = y } } // 每次都會從新建立一個 point 對象 return new Point(x,y) } function length(point) { //... } 複製代碼
但願你們學到了一些 V8 底層的知識,知道如何去編寫更優的 JavaScript 代碼。