Event Loop 是什麼?
Event Loop 是什麼?
本文寫於 2020 年 12 月 6 日
一個場景
Event Loop 並不是 JavaScript 獨有的概念,他是一個計算機的通用概念。
為什麼需要 Event Loop 呢?先看一個常見的場景,如果我們同時執行了三種不同的非同步事件:
setTimeout(foo, 100);
fs.readFile('./README.md', bar);
server.on('close', doSth);
我們知道在計算機中,作業系統會幫我們新建一個「程序」使應用程式可以執行,但是一個程序在一個時刻只能執行一個任務,如果我們需要同時執行這三個任務,有三個方法:
- 新建一個程序;
- 新建一個執行緒;
- 排隊執行。
如果不太瞭解,可以看我之前的一篇文章,「程序與執行緒」。
JavaScript 作為一門單執行緒的語言,肯定不能分出三條執行緒去執行這三條語句。那麼設計時器結束的一瞬間,三個回撥函式同時觸發了,Node 會怎麼處理呢?
PS:Node.js 讀取檔案用的是 libuv,自己並不會去讀寫檔案,所以單執行緒也可以非同步的讀寫檔案
我們可以推測出以下兩點:
- Node 肯定會以某種順序執行;
- 這種順序應該是規定好的(優先順序)。
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 在這個過程中是如何處理的呢?
- 當我們的 Event Loop 進入 poll 階段,發現 poll 的佇列是空的,因為檔案沒有讀完。
- 於是 Event Loop 檢查了一下最近的計時器,發現還需要 100ms,於是決定這段時間就停留在 poll 階段。
- 在 poll 階段停留了 95ms 之後,
readFile
操作完成,耗時 10ms 的同步操作被系統放入 poll 佇列,執行完畢之後 poll 佇列為空。 - 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 的回撥
(完)