JS引擎線程的執行過程的三個階段


瀏覽器首先按順序加載由<script>標籤分割的js代碼塊,加載js代碼塊完畢後,馬上進入如下三個階段,而後再按順序查找下一個代碼塊,再繼續執行如下三個階段,不管是外部腳本文件(不異步加載)仍是內部腳本代碼塊,都是同樣的原理,而且都在同一個全局做用域中。node

JS引擎線程的執行過程的三個階段:git

  • 語法分析
  • 預編譯階段
  • 執行階段

一. 語法分析

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

下面階段的代碼執行不會再進行語法校驗,語法分析在代碼塊加載完畢時統一檢驗語法。github

二. 預編譯階段

1. js的運行環境

  • 全局環境(JS代碼加載完畢後,進入代碼預編譯即進入全局環境)chrome

  • 函數環境(函數調用執行時,進入該函數環境,不一樣的函數則函數環境不一樣)promise

  • eval(不建議使用,會有安全,性能等問題)瀏覽器

每進入一個不一樣的運行環境都會建立一個相應的執行上下文(Execution Context),那麼在一段JS程序中通常都會建立多個執行上下文,js引擎會以棧的方式對這些執行上下文進行處理,造成函數調用棧(call stack),棧底永遠是全局執行上下文(Global Execution Context),棧頂則永遠是當前執行上下文。安全

2. 函數調用棧/執行棧

調用棧,也叫執行棧,具備LIFO(後進先出)結構,用於存儲在代碼執行期間建立的全部執行上下文。bash

首次運行JS代碼時,會建立一個全局執行上下文並Push到當前的執行棧中。每當發生函數調用,引擎都會爲該函數建立一個新的函數執行上下文並Push到當前執行棧的棧頂。數據結構

當棧頂函數運行完成後,其對應的函數執行上下文將會從執行棧中Pop出,上下文控制權將移到當前執行棧的下一個執行上下文。

var a = 'Hello World!';

function first() {  
  console.log('Inside first function');  
  second();  
  console.log('Again inside first function');  
}

function second() {  
  console.log('Inside second function');  
}

first();  
console.log('Inside Global Execution Context');

// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context複製代碼

函數調用棧

3. 執行上下文的建立

執行上下文可理解爲當前的執行環境,與該運行環境相對應,具體分類如上面所說分爲全局執行上下文和函數執行上下文。建立執行上下文的三部曲:

  • 建立變量對象(Variable Object)

  • 創建做用域鏈(Scope Chain)

  • 肯定this的指向

3.1 建立變量對象

建立變量對象

  • 建立arguments對象:檢查當前上下文中的參數,創建該對象的屬性與屬性值,僅在函數環境(非箭頭函數)中進行,全局環境沒有此過程

  • 檢查當前上下文的函數聲明:按代碼順序查找,將找到的函數提早聲明,若是當前上下文的變量對象沒有該函數名屬性,則在該變量對象以函數名創建一個屬性,屬性值則爲指向該函數所在堆內存地址的引用,若是存在,則會被新的引用覆蓋。

  • 檢查當前上下文的變量聲明:按代碼順序查找,將找到的變量提早聲明,若是當前上下文的變量對象沒有該變量名屬性,則在該變量對象以變量名創建一個屬性,屬性值爲undefined;若是存在,則忽略該變量聲明

函數聲明提早和變量聲明提高是在建立變量對象中進行的,且函數聲明優先級高於變量聲明。具體是如何函數和變量聲明提早的能夠看後面。

建立變量對象發生在預編譯階段,但還沒有進入執行階段,該變量對象都是不能訪問的,由於此時的變量對象中的變量屬性還沒有賦值,值仍爲undefined,只有進入執行階段,變量對象中的變量屬性進行賦值後,變量對象(Variable Object)轉爲活動對象(Active Object)後,才能進行訪問,這個過程就是VO –> AO過程。

3.2 創建做用域鏈

通俗理解,做用域鏈由當前執行環境的變量對象(未進入執行階段前)與上層環境的一系列活動對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。

能夠經過一個例子簡單理解:

var num = 30;

function test() {
    var a = 10;

    function innerTest() {
        var b = 20;

        return a + b
    }

    innerTest()
}

test()複製代碼

在上面的例子中,當執行到調用innerTest函數,進入innerTest函數環境。全局執行上下文和test函數執行上下文已進入執行階段,innerTest函數執行上下文在預編譯階段建立變量對象,因此他們的活動對象和變量對象分別是AO(global),AO(test)和VO(innerTest),而innerTest的做用域鏈由當前執行環境的變量對象(未進入執行階段前)與上層環境的一系列活動對象組成,以下:

innerTestEC = {

    //變量對象
    VO: {b: undefined}, 

    //做用域鏈
    scopeChain: [VO(innerTest), AO(test), AO(global)],  
    
    //this指向
    this: window
}複製代碼

深刻理解的話,建立做用域鏈,也就是建立詞法環境,而詞法環境有兩個組成部分:

  • 環境記錄:存儲變量和函數聲明的實際位置
  • 對外部環境的引用:能夠訪問其外部詞法環境

詞法環境類型僞代碼以下:

// 第一種類型: 全局環境
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 定義的變量就會提示引用錯誤的緣由。此時let 和 const處於未初始化狀態不能使用,只有進入執行階段,變量對象中的變量屬性進行賦值後,變量對象(Variable Object)轉爲活動對象(Active Object)後,letconst才能進行訪問。

關於函數聲明和變量聲明,這篇文章講的很好:github.com/yygmind/blo…

另外關於閉包的理解,如例子:

function foo() {
    var num = 20;

    function bar() {
        var result = num + 20;

        return result
    }

    bar()
}

foo()複製代碼

瀏覽器分析以下:

閉包

chrome瀏覽器理解閉包是foo,那麼按瀏覽器的標準是如何定義閉包的,總結爲三點:

  • 在函數內部定義新函數

  • 新函數訪問外層函數的局部變量,即訪問外層函數環境的活動對象屬性

  • 新函數執行,建立新的函數執行上下文,外層函數即爲閉包

3.3 this指向

比較複雜,後面專門弄一篇文章來整理。

三. 執行階段

1. 網頁的線程

永遠只有JS引擎線程在執行JS腳本程序,其餘三個線程只負責將知足觸發條件的處理函數推動事件隊列,等待JS引擎線程執行, 不參與代碼解析與執行。

  • JS引擎線程: 也稱爲JS內核,負責解析執行Javascript腳本程序的主線程(例如V8引擎)

  • 事件觸發線程: 歸屬於瀏覽器內核進程,不受JS引擎線程控制。主要用於控制事件(例如鼠標,鍵盤等事件),當該事件被觸發時候,事件觸發線程就會把該事件的處理函數推動事件隊列,等待JS引擎線程執行

  • 定時器觸發線程:主要控制計時器setInterval和延時器setTimeout,用於定時器的計時,計時完畢,知足定時器的觸發條件,則將定時器的處理函數推動事件隊列中,等待JS引擎線程執行。 注:W3C在HTML標準中規定setTimeout低於4ms的時間間隔算爲4ms。

  • HTTP異步請求線程:經過XMLHttpRequest鏈接後,經過瀏覽器新開的一個線程,監控readyState狀態變動時,若是設置了該狀態的回調函數,則將該狀態的處理函數推動事件隊列中,等待JS引擎線程執行。 注:瀏覽器對通一域名請求的併發鏈接數是有限制的,Chrome和Firefox限制數爲6個,ie8則爲10個。

2. 宏任務

宏任務(macro-task)可分爲同步任務異步任務

  • 同步任務指的是在JS引擎主線程上按順序執行的任務,只有前一個任務執行完畢後,才能執行後一個任務,造成一個執行棧(函數調用棧)。

  • 異步任務指的是不直接進入JS引擎主線程,而是知足觸發條件時,相關的線程將該異步任務推動任務隊列(task queue),等待JS引擎主線程上的任務執行完畢,空閒時讀取執行的任務,例如異步Ajax,DOM事件,setTimeout等。

理解宏任務中同步任務和異步任務的執行順序,那麼就至關於理解了JS異步執行機制–事件循環(Event Loop)。

3. 事件循環

事件循環能夠理解成由三部分組成,分別是:

  • 主線程執行棧

  • 異步任務等待觸發

  • 任務隊列

任務隊列(task queue)就是以隊列的數據結構對事件任務進行管理,特色是先進先出,後進後出。

事件循環

setTimeout和setInterval的區別:

  • setTimeout是在到了指定時間的時候就把事件推到任務隊列中,只有當在任務隊列中的setTimeout事件被主線程執行後,纔會繼續再次在到了指定時間的時候把事件推到任務隊列,那麼setTimeout的事件執行確定比指定的時間要久,具體相差多少跟代碼執行時間有關

  • setInterval則是每次都精確的隔一段時間就向任務隊列推入一個事件,不管上一個setInterval事件是否已經執行,因此有可能存在setInterval的事件任務累積,致使setInterval的代碼重複連續執行屢次,影響頁面性能。

4. 微任務

微任務是在es6和node環境中出現的一個任務類型,若是不考慮es6和node環境的話,咱們只須要理解宏任務事件循環的執行過程就已經足夠了,可是到了es6和node環境,咱們就須要理解微任務的執行順序了。 微任務(micro-task)的API主要有:Promise, process.nextTick

微任務

例子理解:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');複製代碼

執行過程以下:

  • 代碼塊經過語法分析和預編譯後,進入執行階段,當JS引擎主線程執行到console.log('script start');,JS引擎主線程認爲該任務是同步任務,因此馬上執行輸出script start,而後繼續向下執行;

  • JS引擎主線程執行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主線程認爲setTimeout是異步任務API,則向瀏覽器內核進程申請開啓定時器線程進行計時和控制該setTimeout任務。因爲W3C在HTML標準中規定setTimeout低於4ms的時間間隔算爲4ms,那麼當計時到4ms時,定時器線程就把該回調處理函數推動任務隊列中等待主線程執行,而後JS引擎主線程繼續向下執行

  • JS引擎主線程執行到Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });,JS引擎主線程認爲Promise是一個微任務,這把該任務劃分爲微任務,等待執行

  • JS引擎主線程執行到console.log('script end');,JS引擎主線程認爲該任務是同步任務,因此馬上執行輸出script end

  • 主線程上的宏任務執行完畢,則開始檢測是否存在可執行的微任務,檢測到一個Promise微任務,那麼馬上執行,輸出promise1和promise2

  • 微任務執行完畢,主線程開始讀取任務隊列中的事件任務setTimeout,推入主線程造成新宏任務,而後在主線程中執行,輸出setTimeout

最後的輸出結果即爲:

script start
script end
promise1
promise2
setTimeout複製代碼

文章參考:

github.com/yygmind/blo…

heyingye.github.io/2018/03/19/…

heyingye.github.io/2018/03/26/…

github.com/yygmind/blo…

相關文章
相關標籤/搜索