淺談 JavaScript 中的 Event Queue、Event Table、Event Loop 以及 Event Task

前言

在翻一些文獻時剛好翻到關於 Event Queue 的細節說明,所以就寫一篇來紀錄一下。

Event Queue (事件佇列)

Event Queue 其實就是我們常在講的事件佇列、事件儲列等等名稱,中文翻譯實在太多了,所以接下來都會以 Event Queue 為主。

那麼什麼是 Event Queue 呢?

實際開發時,我們常常會遇到一些非同步的問題,例如:AJAX 操作、setTimeout 呼叫等等,而這些行為就會導致執行順序無法掌握。

首先先讓我們看一段簡易的程式碼,我們預期下面應該是要依照順序出現 a → b → c

1
2
3
console.log('a');
setTimeout(() => console.log('b'), 0);
console.log('c');

但實際上出現卻是…

Example code

WT...

為什麼會這樣呢?以上面範例來講 setTimeoutconsole.log 非常大的不同在於 setTimeout 並不是 直接執行,而是時間到了才執行的 Web APIs,因此 setTimeout 就會被丟到 Event Queue 中。

因此我們常見的 AJAX 也會有這種狀況,畢竟 AJAX 取資料的時間是不確定的:

1
2
3
4
5
6
7
console.log('Start');
console.log('A');
console.log('A');
axios.get('https://randomuser.me/api/').then((res) => console.log(res.data.results[0].name.last))
console.log('B');
console.log('C');
console.log('End');

Event Table (事件表格)

那麼 Event Queue 中還有一個東西是 Event Table,而 Event Table 用處在於紀錄準備丟進 Event Queue 的東西

所以這邊先讓我們看一下即將要講的範例程式碼:

1
2
3
console.log('a');
setTimeout(() => console.log('b'), 0);
console.log('c');

剛剛我們有看到範例結果是如下:

Example code

那這一段運作是如何呢?讓我們用圖片來拆解吧~

首先 console.log('a');setTimeout(() => console.log('b'), 0);console.log('c'); 會被放在一個地方,稱之為 Call Stack

Call Stack

那麼接下來 JavaScript 會將 setTimeout(() => console.log('b'), 0); 放到 Event Table 註冊這個事件

Event Table

這邊 Event Table 會將 setTimeout 紀錄 0 秒後執行並放在 Event Table 中,當放在 Event Table 的函式時間到了之後,就會將 Event Table 中這個函式放到 Event Queue 等待被丟回 Call Stack

Event Queue

所以 Event Queue 也就是一個緩衝區,準備回到 Call Stack 的地方。

Event Loop (事件循環)

接下來會有一個機制一直監聽 Event Queue 裡面有沒有東西要丟出來到 Call Stack,當若有東西的話它就會丟到 Call Stack

Event Loop

當發現裡面若有東西時,它就會將結果丟回到 Call Stack

Call Stack

所以你才會看到這個結果

Example code

Event Task (事件任務)

還有一個東西是 Event Task,而 Event Task 細分兩種,分別是

  • MacroTask (宏任務)
  • MicroTask (微任务)

那麼這兩者有什麼差別呢?這邊先讓我們看一段範例程式碼

1
2
3
4
console.log('Start');
Promise.resolve().then(() => { console.log('Promise') });
setTimeout(() => { console.log('setTimeout') }, 0);
console.log('End');

Example Code

我們可以看到順序是 Start → End → Promise → setTimeout

但如果程式碼改成以下呢?

1
2
3
4
console.log('Start');
setTimeout(() => { console.log('setTimeout') }, 0);
Promise.resolve().then(() => { console.log('Promise') });
console.log('End');

Example Code

WT...再來一次

那麼為什麼會這樣子呢?原因是出在 MicroTask 與 MacroTask 導致,而 MicroTask 與 MacroTask 所代表的意思是執行順序,相信剛才前面的程式碼我們有發現不管怎麼調換位子 Promise 總是會優先出現,那麼在執行順序上 MicroTask 會優先被執行,接下來才會執行 MacroTask。

但是哪些語法是屬於 MacroTask 以及哪些是屬於 MicroTask 呢?基本上你只需要知道 MicroTask 主要是以下語法就好

  • Promise
  • process.nextTick (Node.js)
  • MutaionObserver

其他則是屬於 MacroTask,因此整體運作過程會像這樣:

MicroTask & MacroTask

而這一整個循環也就是 Event Loop 唷~

最後也補一下 MacroTask 有哪些當作結尾:

  • script(JavaScript 整體程式碼)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI 互動相關

參考文獻