【JavaScript】Event Loop

【JavaScript】Event Loop

原文鏈接:《從 JS Event Loop 機制看 Vue 中 nextTick 的實現原理》

Event Loop 即事件循環機制,是理解 JavaScript 運行機制的最關鍵的一點,文章中通過拋出一道題來引入這節課所要講解的內容。

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

new Promise(function executor(resolve) {
  console.log(2);
  for( var i=0 ; i<10000 ; i++ ) {
    i == 9999 && resolve();
  }
  console.log(3);
}).then(function() {
  console.log(4);
});

console.log(5);

// result: 2, 3, 5, 4, 1

單線程的 JavaScript

  • 定義:所謂單線程,是指在 JS 引擎中負責解釋和執行 JavaScript 代碼的線程只有一個;

  • 特點:JS 運行在瀏覽器中,是單線程的,每個 window 一個線程;

  • 原因:若爲多線程,在 dom 操作中會產生混亂,如 A 線程修改 dom,B 線程卻刪除了這個 dom;

  • 效率:JavaScript 中有很多其他的類線程,也成爲異步事件,如:Ajax請求,監控用戶事件,定時器,讀寫文件等等。

  • 過程:當異步事件發生時,將他們放入執行隊列,(主線程)等待當前代碼執行完成。就不會長時間阻塞主線程。等主線程的代碼執行完畢,然後再讀取任務隊列,返回主線程繼續處理。如此循環這就是事件循環機制。

JavaScript 的內存空間

  • 棧數據結構

    • 結構:棧數據結構
    • 特點:先進後出,後進先出(FILO)
  • 堆數據結構

    • 結構:key - value 結構;
    • 特點:存儲的 key - value 是無序的,通過 key 取出,無需關心順序;
  • 隊列數據結構

    • 結構:隊列數據結構
    • 特點:先進先出,後進後出(FIFO)

執行上下文 (Execution Context) & 函數調用棧

  • 每當控制器轉到可執行代碼的時候,就會進入一個執行上下文;
  • 運行環境:
    • 全局環境:JavaScript 代碼運行起來會首先進入該環境;
    • 函數環境:當函數被調用執行時,會進入當前函數中執行代碼;
  • 棧底永遠都是全局上下文,而棧頂就是當前正在執行的上下文;
  • 需要注意的是:函數中,遇到 return 關鍵字能直接終止可執行代碼的執行,因此會直接將當期那上下文彈出棧;
  • 總結:
    • 單線程,依次自頂而下執行,遇到函數就會創建函數執行上下文,併入棧;
    • 同步執行,只有棧頂的上下文處於執行中,其他上下文需等待;
    • 全局上下文只有一個,它在瀏覽器關閉時出棧;
    • 函數執行上下文的個數沒有限制;
    • 每次某個函數被調用,就會有個新的執行上下文爲其創建,即使是調用的自身函數,也是如此。

事件循環

  • 在 JavaScript 代碼執行的過程中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列來搞定另一些代碼的執行;

  • 特點:即任務隊列的特點,先進先出;

  • 圖例:隊列數據結構

  • 任務隊列:一個 JS 文件裏事件循環只有一個,但是任務隊列可以有多個,因此任務隊列可以分爲:

    • macro-task (task)

      // macro-task (task) 包括:
      1. setTimeout / setInterval;
      2. setImmediate;
      3. I/O operation;
      4. UI Rendering
      
    • micro-task (job)

      // micro-task (job) 包括:
      1. process.nextTick;
      2. Promise;
      3. Object.observe (已廢棄);
      4. MutationObserver(html5新特性);
      
  • 以上這些我們稱他們爲事件源,事件源作爲任務分發器,他們的回調函數纔是被分發到任務隊列,而本身會立即執行,例如:setTimeout 第一個參數被分發到任務隊列,Promisethen 方法的回調函數被分發到任務隊列( catch 方法同理);

  • 不同事件源的事件被分發到不同的任務隊列,其中 setTimeout 和 setInterval 屬於同源;

  • 流程:整體代碼開始第一次循環。全局上下文進入函數調用棧。直到調用棧清空(只剩全局),然後執行所有的 job。當所有可執行的 job 執行完畢之後。循環再次從 task 開始,找到其中一個任務隊列執行完畢,然後再執行所有的 job,這樣一直循環下去。

  • 規律:task–job–task–job…,往復循環直到沒有可執行代碼

  • 有趣的栗子:

console.log(1);

// 執行到此, Promise 的回調是同步執行,then / catch 纔會被分發到 job 中
new Promise(function(resolve){
    console.log(2);
    resolve();
}).then(function(){
    console.log(3)
})

// 執行到此,setTimeout 執行將回調 function 分發至 task 中
setTimeout(function(){
    console.log(4);
    process.nextTick(function(){
        console.log(5);
     })
    new Promise(function(resolve){
        console.log(6);
        resolve()
    }).then(function(){
        console.log(7)
    })
})

// 執行到此, process.nextTick 的回調會被分發到 job 中
process.nextTick(function(){
    console.log(8)
})

// 同 setTimeout 原理相同
setImmediate(function(){
    console.log(9);
    new Promise(function(resolve){
            console.log(10);
            resolve()
        }).then(function(){
            console.log(11)
        })
       process.nextTick(function(){
           console.log(12);
        })
})

// 最後結果: 1, 2, 8, 3, 4, 6, 5, 7, 9, 10, 12, 11
  • 注意:

    • 執行遇到了 PromisePromise 構造函數的回調函數是同步執行;
    • nextTick 任務隊列會比 Promise 的隊列先執行;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章