「四」瀏覽器中js引擎解析過程(看完秒懂!)

我們前幾章和講解了什麼瀏覽器的組成部分以及渲染引擎,今天我們主要講一下js引擎的相關知識點,那麼在開講之前我們需要回顧一下有關渲染引擎的相關知識點

渲染引擎

關鍵渲染路徑是指瀏覽器從最初接收請求來的HTML、CSS、javascript等資源,然後解析、構建樹、渲染布局、繪製,最後呈現給客戶能看到的界面這整個過程。


JavaScript引擎

JavaScript引擎是一個專門處理JavaScript腳本的虛擬機,一般會附帶在網頁瀏覽器中。JavaScript引擎從頭到尾負責整個JavaScript程序的編譯和執行過程。js的引擎有很多種,而最爲大家熟知的無疑是V8引擎,他用於Chrome瀏覽器和Node中。

V8引擎由兩個主要部件組成:

emory Heap(內存堆) — 內存分配地址的地方
Call Stack(調用堆棧) — 代碼執行的地方

上面只是簡單的對js引擎進行一下基本的瞭解,下面開始正式介紹js引擎的執行過程(以V8引擎爲例)

js引擎執行過程

全面分析js引擎的執行過程,主要分爲三個階段:

1. 語法分析
2. 預編譯階段
3. 執行階段

下面着重講一下這三個階段:

語法分析

分析該js腳本代碼塊的語法是否正確,如果出現不正確,則向外拋出一個語法錯誤(SyntaxError),停止該js代碼塊的執行,然後繼續查找並加載下一個代碼塊;如果語法正確,則進入預編譯階段

預編譯階段

js代碼塊通過語法分析階段之後,語法都正確的下回進入預編譯階段。
在分析預編譯階段之前,我們先來了解一下js的運行環境,運行環境主要由三種:
1、全局環境(js代碼加載完畢後,進入到預編譯也就是進入到全局環境)
2、函數環境(函數調用的時候,進入到該函數環境,不同的函數,函數環境不同)
3、eval環境(不建議使用,存在安全、性能問題)

每進入到一個不同的運行環境都會創建一個相應的執行上下文(execution context)「下文會介紹」,那麼在一段js程序中一般都會創建多個執行上下文,js引擎會以棧的數據結構對這些執行進行處理,形成函數調用棧(call stack),棧底永遠是全局執行上下文(global execution context),棧頂則永遠時當前的執行上下文
注意:執行上下文的相關概念會在下面進一步進行詳細介紹

執行階段

在執行階段,我們暫時先不考慮異步(因爲異步階段涉及到的知識點是事件循環【event loop】),等讀完這篇文章之後並且理解之後,去看我寫的深入理解事件循環這篇文章,就會進一步理解。

我們上文講到V8引擎由兩個主要部件組成:
emory Heap(內存堆) — 內存分配地址的地方
Call Stack(調用堆棧)— 代碼執行的地方

我們聲明的函數與變量被儲存在『內存堆』中,而當我們要執行的時候,就必須藉助於『調用棧』來解決問題。函數調用棧就是使用棧存取的方式進行管理運行環境,特點是先進後出,後進後出

我們來分析一下js代碼來理解函數調用棧:

function bar() {
    var B_context = "bar saucxs";

    function foo() {
        var f_context = "foo saucxs";
    }

    foo()
}

bar()

上面代碼塊通過語法分析後,進入預編譯階段創建執行上下文,如圖所示


1、首先進入到全局環境,創建全局執行上下文(global Execution Context ),推入到stack中;
2、調用bar函數,進入bar函數運行環境,創建bar函數執行上下文(bar Execution Context),推入stack棧中;
3、在bar函數內部調用foo函數,則再進入到foo函數運行環境中,創建foo函數執行上下文(foo Execution Context),如上圖,由於foo函數內部沒有再調用其他函數,那麼則開始出棧;
5、foo函數執行完畢之後,棧頂foo函數執行上下文(foo Execution Context)首先出棧;
6、bar函數執行完畢,bar函數執行上下文(bar Execution Context)出棧;
7、全局上下文(global Execution Cntext)在瀏覽器或者該標籤關閉的時候出棧。

說明:不同的運行環境執行都會進入到代碼預編譯和執行兩個階段,語法分析則在代碼塊加載完畢時統一檢查語法。

上面的就是我們簡單的對一段代碼進行分析的過程,下面,我們講一下在預編譯階段提到的執行上下文

執行上下文

執行上下文可理解爲當前的執行環境,與該運行環境相對應.前面我們提到過,JavaScript中有三種可執行代碼塊,當然也對應着三種執行上下文。

  • 全局執行上下文
    這是基礎上下文,任何不在函數內部的代碼都在全局上下文中。一個程序中只會有一個全局執行上下文。它會執行兩件事:創建一個全局的 window 對象(瀏覽器的情況下),並且設置 this 的值等於這個全局對象。

  • 函數執行上下文
    每當一個函數被調用時, 都會爲該函數創建一個新的上下文。每個函數都有它自己的執行上下文,不過是在函數被調用時創建的。函數上下文可以有任意多個。每當一個新的執行上下文被創建。

  • Eval 執行上下文
    執行在 eval 內部的代碼也會有它屬於自己的執行上下文,除非你想搞黑魔法,不然不要輕易使用它。

執行上下文分爲兩個階段:
  • 創建階段
  • 執行階段

我們主要討論創建階段,執行階段的主要工作就是分配變量

創建階段

創建執行上下文的過程中,主要是做了下面三件事,如圖所示:

1、確定 this 的值,也被稱爲 This Binding。
2、LexicalEnvironment(詞法環境) 組件被創建。
3、VariableEnvironment(變量環境) 組件被創建。

This Binding
  • 全局執行上下文中,this 的值指向全局對象,在瀏覽器中this 的值指向 window對象,而在nodejs中指向這個文件的module對象。

  • 函數執行上下文中,this 的值取決於函數的調用方式。具體有:默認綁定、隱式綁定、顯式綁定(硬綁定)、new綁定、箭頭函數,具體內容請參考JavaScript深入之史上最全--5種this綁定全面解析這篇文章

詞法環境有兩個組成部分
  • 1、環境記錄:存儲變量和函數聲明的實際位置

  • 2、對外部環境的引用:可以訪問其外部詞法環境

詞法環境有兩種類型

  • 1、全局環境:是一個沒有外部環境的詞法環境,其外部環境引用爲 null。擁有一個全局對象(window 對象)及其關聯的方法和屬性(例如數組方法)以及任何用戶自定義的全局變量,this 的值指向這個全局對象。

  • 2、函數環境:用戶在函數中定義的變量被存儲在環境記錄中,包含了arguments 對象。對外部環境的引用可以是全局環境,也可以是包含內部函數的外部函數環境。

直接看僞代碼可能更加直觀

GlobalExectionContext = {  // 全局執行上下文
  LexicalEnvironment: {       // 詞法環境
    EnvironmentRecord: {        // 環境記錄
      Type: "Object",              // 全局環境
      // 標識符綁定在這裏 
      outer: <null>                // 對外部環境的引用
  }  
}

FunctionExectionContext = { // 函數執行上下文
  LexicalEnvironment: {       // 詞法環境
    EnvironmentRecord: {        // 環境記錄
      Type: "Declarative",         // 函數環境
      // 標識符綁定在這裏             // 對外部環境的引用
      outer: <Global or outer function environment reference>  
  }  
}

變量環境

變量環境也是一個詞法環境,因此它具有上面定義的詞法環境的所有屬性。
在 ES6 中,詞法 環境和 變量 環境的區別在於前者用於存儲函數聲明和變量( letconst綁定,而後者僅用於存儲變量( var綁定。

使用例子進行介紹

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

執行上下文如下所示

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 標識符綁定在這裏  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 標識符綁定在這裏  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

變量提升的原因:在創建階段,函數聲明存儲在環境中,而變量會被設置爲 undefined(在 var 的情況下)或保持未初始化(在 letconst 的情況下)。所以這就是爲什麼可以在聲明之前訪問 var 定義的變量(儘管是 undefined ),但如果在聲明之前訪問 letconst 定義的變量就會提示引用錯誤的原因。這就是所謂的變量提升。

執行上下文的屬性

對於每個執行上下文,都有三個重要屬性:

  • 變量對象(Variable object,VO)
  • 作用域鏈(Scope chain)
  • this

下面我們逐一簡略講解下:

變量對象

變量對象是與執行上下文相關的數據作用域,存儲了在上下文中定義的變量和函數聲明。
不同執行上下文下的變量對象稍有不同
全局上下文中的變量對象是全局對象
函數上下文:
在函數上下文中,我們用活動對象(activation object, AO)來表示變量對象。
活動對象和變量對象其實是一個東西,但是兩者的區別在於

  • 1、變量對象(VO)是規範上或者是JS引擎上實現的,並不能在JS環境中直接訪問。
  • 2、當進入到一個執行上下文後,這個變量對象纔會被激活,所以叫活動對象(AO),這時候活動對象上的各種屬性才能被訪問。

活動對象是在進入函數上下文時被創建的,它通過函數的 arguments 屬性初始化。arguments 屬性值是 Arguments 對象。

函數上下文的執行過程

執行上下文的代碼會分成兩個階段進行處理:分析和執行,我們也可以叫做:

  • 進入執行上下文
  • 代碼執行
進入執行上下文

當進入執行上下文時,這時候還沒有執行代碼,變量對象會包括:

  • 1、函數的所有形參 (如果是函數上下文):由名稱和對應值組成的一個變量對象的屬性被創建,如果沒有實參,屬性值設爲undefined
  • 2、函數聲明:由名稱和對應值(函數對象(function-object))組成一個變量對象的屬性被創建,如果變量對象已經存在相同名稱的屬性,則完全替換這個屬性
  • 3、變量聲明:由名稱和對應值(undefined)組成一個變量對象的屬性被創建,如果變量名稱跟已經聲明的形參或函數相同,則變量聲明不會干擾已經存在的這類屬性
代碼執行

在代碼執行階段,會順序執行代碼,根據代碼,修改變量對象的值
舉個例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}
foo(1);

在進入執行上下文後,這時候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

代碼執行的時候變成:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

下面思考這兩道題目:輸出的會是什麼?

var foo = function () {
    console.log('foo1');
}
foo();  // foo1
var foo = function () {
    console.log('foo2');
}
foo(); // foo2

然而去看這段代碼:

function foo() {
    console.log('foo1');
}
foo();  // foo2
function foo() {
    console.log('foo2');
}
foo(); // foo2

這是因爲在進入執行上下文時,首先會處理函數聲明,其次會處理變量聲明,如果如果變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。

我們上文講了每個執行上下文都有三個重要的屬性,變量對象,作用域和this,由於作用域和this的篇幅過於長,所以,暫時不在這篇文章做展示

參考文檔

https://www.cxymsg.com/guide/mechanism.html#javascript%E7%9A%84%E6%89%A7%E8%A1%8C%E7%8E%AF%E5%A2%83
https://segmentfault.com/a/1190000017812175
https://juejin.im/post/5dde27615188256ebd1618fb
https://muyiy.cn/blog/1/1.1.html
https://github.com/mqyqingfeng/Blog/issues/5

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章