《圖解 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 的錯誤。

相關文章