1. 程式人生 > 實用技巧 >Event Loop 是什麼?

Event Loop 是什麼?

Event Loop 是什麼?

本文寫於 2020 年 12 月 6 日

一個場景

Event Loop 並不是 JavaScript 獨有的概念,他是一個計算機的通用概念。

為什麼需要 Event Loop 呢?先看一個常見的場景,如果我們同時執行了三種不同的非同步事件:

setTimeout(foo, 100);
fs.readFile('./README.md', bar);
server.on('close', doSth);

我們知道在計算機中,作業系統會幫我們新建一個「程序」使應用程式可以執行,但是一個程序在一個時刻只能執行一個任務,如果我們需要同時執行這三個任務,有三個方法:

  1. 新建一個程序;
  2. 新建一個執行緒;
  3. 排隊執行。

如果不太瞭解,可以看我之前的一篇文章,「程序與執行緒」。

JavaScript 作為一門單執行緒的語言,肯定不能分出三條執行緒去執行這三條語句。那麼設計時器結束的一瞬間,三個回撥函式同時觸發了,Node 會怎麼處理呢?

PS:Node.js 讀取檔案用的是 libuv,自己並不會去讀寫檔案,所以單執行緒也可以非同步的讀寫檔案

我們可以推測出以下兩點:

  1. Node 肯定會以某種順序執行;
  2. 這種順序應該是規定好的(優先順序)。

Event Loop 的解釋

回到 Event Loop 本身,我們拆開來看看。什麼叫做 Event?Event 就是事件,比如回撥函式的觸發就是一個事件。什麼叫做 Loop?Loop 就是迴圈,比如 for 迴圈 while 迴圈。

事件是有優先順序的,所以處理時候是分先後的。Node.js 按照順序去“詢問”每個 Event,並且迴圈往復的去“詢問”:Event1 => Event2 => Event3 => Event1 => Event2 => ...這就變成了 「poll(輪詢)」

作業系統觸發事件,JavaScript 處理事件,Event Loop 就是事件處理順序管理的解決方案。

維基是這麼解釋的:Event Loop 是一個程式結構,用於等待和傳送訊息和事件。

簡單說,就是在程式中設定兩個執行緒:

  • 一個負責程式本身的執行,稱為"主執行緒";
  • 另一個負責主執行緒與其他程序的通訊,被稱為 「Event Loop 執行緒」
    ,也有人叫他「訊息執行緒」。

Node 中的 Event Loop

     |-----------------------|
---->|         timer         |
|    |-----------------------|
|                |
|                |
|    |-----------------------|
|    |    close callbacks    |
|    |-----------------------|
|                |
|                |
|    |-----------------------|
|    |     idle, prepare     |
|    |-----------------------|
|                |
|                |                 |------------------|
|    |-----------------------|     |     incoming:    |
|    |          poll         |<----|    connections,  |
|    |-----------------------|     |     data, etc.   |
|                |                 |------------------|
|                |
|    |-----------------------|
|    |         check         |
|    |-----------------------|
|                |
|                |
|    |-----------------------|
-----|    close callbacks    |
     |-----------------------|

這是 Node.js 官方文件上的示意圖,我們來分析一下。

  • 首先第一步,Node 會先去尋找是否存在計時器;
  • 然後在看有沒有其他的 I/O 相關的回撥函式;
  • idle 和 prepare 階段根據字面意思推斷,應該是清理一下,休息一下;
  • 下一步進入輪詢的階段,檢查系統事件;
  • check 階段檢查 setImmediate 回撥;
  • close callbacks 就是處理類似 socket 關閉的事件。

這裡可以看到 check 階段檢查的是 setImmediate 回撥,有的面試題目可能會問 setImmediate(fn)setTimeout(fn, 0) 誰先執行,這裡我們就能回答了:不確定,因為 Event Loop 是個圈。

另外,setImmediate 不是一個標準特性,MDN 不建議我們在生產環境中使用它,瀏覽器只有新版的 IE 支援。

在 Node 的 Event Loop 中,最常用的就是:timer 檢查、poll 輪詢、check 檢查三個步驟。

並且大部分時間,Node.js 都停留在 poll 階段,檔案請求、網路請求的事件處理也多半在這個階段進行。所以 Node.js 會很智慧的在其他階段空閒時只停留在該階段。

好吧,可能看不太懂這個圖。

我們看官方例子:

const fs = require('fs');

// 假設花費 95ms 讀取檔案
function someAsyncOperation() {
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms 後執行了 setTimeout 的回撥函式`);
}, 100);

// 95ms 的非同步操作
someAsyncOperation(() => {
  const startCallback = Date.now();
  // 10ms 的同步操作
  while (Date.now() - startCallback < 10) {
    // nothing to do
  }
});

這裡我們花 95ms 讀完檔案後又做了 10ms 的同步操作。但問題是在 95ms 結束後 5ms 就需要執行定時器了呀,這裡硬生生被 while 的同步操作卡住了 5ms,Node.js 在這個過程中是如何處理的呢?

  1. 當我們的 Event Loop 進入 poll 階段,發現 poll 的佇列是空的,因為檔案沒有讀完。
  2. 於是 Event Loop 檢查了一下最近的計時器,發現還需要 100ms,於是決定這段時間就停留在 poll 階段。
  3. 在 poll 階段停留了 95ms 之後,readFile 操作完成,耗時 10ms 的同步操作被系統放入 poll 佇列,執行完畢之後 poll 佇列為空。
  4. Event Loop 緊接著又去看了一眼計時器,發現已經超時了 5ms 了。於是由經 check 階段,close callbacks 階段,Event Loop 又回到了 timer 階段執行了超時計時器的回撥函式。

這裡如果我們一直佔著 poll 階段做同步任務會怎麼樣呢?

Node.js 做了限制,會有最長佔用時長的,根據作業系統而定。

瀏覽器的 Event Loop

瀏覽器只有巨集佇列和微佇列,比 Node.js 簡單很多。(注意,這都不是 JS 引擎提供的,而是 Node 和瀏覽器提供的)。

  • 巨集列隊:用來儲存待執行的 macrotask (巨集任務),比如:timer / DOM 事件監聽 / ajax 回撥;
  • 微列隊:用來儲存待執行的 microtask(微任務),比如:Promise 的回撥 / MutationObserver 的回撥

(完)