《圖解 Google V8》設計思想篇——學習筆記(一)

uccs發表於2023-02-07

這是《圖解 Google V8》第一篇/共三篇:設計思想篇

這個專欄的優點是:寫的通俗易懂,沒有涉及 V8 原始碼部分,對於前端還是比較友好的,學完之後能夠知道寫下一段 js 程式碼後,V8 背後都做了哪些事情

這個專欄的不足之處是:沒有對技術進行深入講解,只講了這個技術是用來解決什麼問題的,以及它是怎麼工作的

所以這個專欄比較時候對 V8 還不瞭解的同學去學習,增加自己的知識面

下面是我自己學習每一章的總結,主要記錄我在這章中學到內容,並不是對這章完整的總結

如何學習谷歌高效能 JavaScript 引擎 V8?

V8 主要涉及三個技術:編譯流水線、事件迴圈系統、垃圾回收機制

  1. V8 執行 JavaScript 完整流程稱為:編譯流水線
    編譯流水線

    編譯流水線涉及到的技術有:

    • JIT

      • V8 混合編譯執行和解釋執行
    • 惰性解析

      • 加快程式碼啟動速度
    • 隱藏類(Hide Class)

      • 將動態型別轉為靜態型別,消除動態型別執行速度慢的問題
    • 內聯快取
  2. 事件迴圈系統

    • JavaScript 中的難點:非同步程式設計
    • 排程排隊任務,讓 JavaScript 有序的執行
  3. 垃圾回收機制

    • 記憶體分配
    • 記憶體回收

01 | V8 是如何執行一段 JavaScript 程式碼的?

  1. 準備基礎環境:

    • 全域性執行上下文:全域性作用、全域性變數、內建函式
    • 初始化記憶體中的堆和棧結構
    • 初始化訊息迴圈系統:訊息驅動器和訊息佇列
  2. 結構化 JavaScript 原始碼

    • 生成抽象語法樹(AST
    • 生成相關作用域
  3. 生成位元組碼:位元組碼是介於 AST 和機器碼的中間程式碼

    • 直譯器可以直接執行
    • 編譯器需要將其編譯為二進位制的機器碼再執行
  4. 直譯器和監控機器人

    • 直譯器:按照順序執行位元組碼,並輸出執行結果
    • 監控機器人:如果發現某段程式碼被重複多次執行,將其標記為熱點程式碼
  5. 最佳化熱點程式碼

    • 最佳化編譯器將熱點程式碼編譯為機器碼
    • 對編譯後的機器碼進行最佳化
    • 再次執行到這段程式碼時,將優先執行最佳化後的程式碼
  6. 反最佳化

    • JavaScript 物件在執行時可能會被改變,這段最佳化後的熱點程式碼就失效了
    • 進行反最佳化操作,給到直譯器解釋執行

02 | 函式即物件:一篇文章徹底搞懂 JavaScript 的函式特點

V8 內部為函式物件新增了兩個隱藏屬性:namecode

  • name 為函式名

    • 如果是匿名函式,nameanonymous
  • code 為函式程式碼,以字串的形式儲存在記憶體中

當執行到一個函式呼叫語句時,V8 從函式物件中取出 code 屬性值,然後解釋執行這段函式程式碼

什麼是閉包:將外部變數和函式繫結起來的技術

參考資料:

  1. The story of a V8 performance cliff in React

03 | 快屬性和慢屬性:V8 是怎樣提升物件屬性訪問速度的?

V8 在實現物件儲存時,沒有完全採用字典的儲存方式,因為字典是非線性的資料結構,查詢效率會低於線性的資料結構

常規屬性和索引屬性

  • 索引屬性(elements):數字屬性按照索引值的大小升序排列
  • 常規屬性(properties):字串根據建立時的順序升序排列

它們都是線性資料結構,分別為 elements 物件和 properties 物件

執行一次查詢:先從 elements 物件中按照順序讀出所有元素,然後再從 properties 物件中讀取所有的元素

快屬性和慢屬性

在訪問一個屬性時,比如:foo.aV8 先查詢出 properties,然後在從 properties 中查詢出 a 屬性

V8 為了簡化這一步操作,把部分 properties 儲存到物件本身,預設是 10 個,這個被稱為物件內屬性

線性資料結構通常被稱為快屬性

線性資料結構進行大量資料的新增和刪除,執行效率是非常低的,所以 V8 會採用慢屬性策略

慢屬性的物件內部有獨立的非線性資料結構(字典)

參考資料:

  1. V8 是怎麼跑起來的 —— V8 中的物件表示
  2. Fast properties in V8

04 | 函式表示式:涉及大量概念,函式表示式到底該怎麼學?

變數提升

js 中有函式宣告的方式有兩種:

  • 函式宣告

    function foo() {
      console.log("foo");
    }
  • 函式表示式

    var foo = function () {
      console.log("foo");
    };

在編譯階段 V8 解析到函式宣告和函式表示式(變數宣告)時:

  • 函式宣告,將其轉換為記憶體中的函式物件,並放到作用域中
  • 變數宣告,將其值設定為 undefined,並當道作用域中

因為在編譯階段,是不會執行表示式的,只會分析變數的定義、函式的宣告

所以如果在宣告前呼叫 foo 函式:

  • 使用函式宣告不會報錯
  • 使用函式表示式會報錯

在編譯階段將所有的變數提升到作用域的過程稱為變數提升

立即執行函式

js 的圓括號 () 可以在中間放一個表示式

中間如果是一個函式宣告,V8 就會把 (function(){}) 看成是函式表示式,執行它會返回一個函式物件

如果在函式表示式後面加上(),就被稱為立即呼叫函式表示式

因為函式立即表示式也是表示式,所以不會建立函式物件,就不會汙染環境

05 |原型鏈:V8 是如何實現物件繼承的?

  • 作用域鏈是沿著函式的作用域一級一級來查詢變數的
  • 原型鏈是沿著物件的原型一級一級來查詢屬性的

js 中實現繼承,是將 __proto__ 指向物件,但是不推薦使用,主要原因是:

  • 這是隱藏屬性,不是標準定義的
  • 使用該屬性會造成嚴重的效能問題

繼承

  1. 用建構函式實現繼承:

    function DogFactory(color) {
      this.color = color;
    }
    DogFactory.prototype.type = "dog";
    const dog = new DogFactory("Black");
    dog.hasOwnProperty("type"); // false
  2. ES6 之後可以透過 Object.create 實現繼承

    const animalType = { type: "dog" };
    const dog = Object.create(animalType);
    dog.hasOwnProperty("type"); // false

new 背後做了這些事情

  1. 幫你在內部建立一個臨時物件
  2. 將臨時物件的 __proto__ 設定為建構函式的原型,建構函式的原型統一叫做 prototype
  3. return 臨時物件
function NEW(fn) {
  return function () {
    var o = { __proto__: fn.prototype };
    fn.apply(o, arguments);
    return o;
  };
}

__proto__prototypeconstructor 區別

prototype 是函式的獨有的;__proto__constructor 是物件獨有的

由於函式也是物件,所以函式也有 __proto__constructor

constructor 是函式;prototype__proto__ 是物件

typeof Object.__proto__; // "object"
typeof Object.prototype; // "object"
typeof Object.constructor; // "function"
let obj = new Object();
obj.__proto__ === Object.prototype;
obj.constructor === Object;

objObject 的例項,所以 obj.constructor === Object

obj 的是物件,Object 是函式,所以 obj.__proto__ === Object.prototype

參考資料:

  1. 用自己的方式(圖)理解 constructorprototype__proto__ 和原型鏈
  2. 面試官問:JS 的繼承

06 |作用域鏈:V8 是如何查詢變數的?

全域性作用域是在 V8 啟動過程中就建立了,且一直儲存在記憶體中不會被銷燬的,直至 V8 退出

而函式作用域是在執行該函式時建立的,當函式執行結束之後,函式作用域就隨之被銷燬掉了

因為 JavaScript 是基於詞法作用域的,詞法作用域就是指,查詢作用域的順序是按照函式定義時的位置來決定的。

詞法作用域是靜態作用域,根據函式在程式碼中的位置來確定的,作用域是在宣告函式時就確定好了

動態作用域鏈是基於呼叫棧的,不是基於函式定義的位置的,可以認為 this 是用來彌補 JavaScript 沒有動態作用域特性的

07 |型別轉換:V8 是怎麼實現 1+“2”的?

V8 會提供了一個 ToPrimitive 方法,其作用是將 ab 轉換為原生資料型別

  1. 先檢測該物件中是否存在 valueOf 方法,如果有並返回了原始型別,那麼就使用該值進行強制型別轉換
  2. 如果 valueOf 沒有返回原始型別,那麼就使用 toString 方法的返回值
  3. 如果 valueOftoString 兩個方法都不返回基本型別值,便會觸發一個 TypeError 的錯誤。

《圖解 Google V8》學習筆記系列

  1. 《圖解 Google V8》編譯流水篇——學習筆記(二)
  2. 《圖解 Google V8》事件迴圈和垃圾回收——學習筆記(三)

相關文章