為何把任務加入到任務佇列理這項動作,必須發生在事件迴圈之外?
一、先理解事件迴圈 (Event Loop) 的工作機制
JavaScript 是單執行緒的,也就是說同一時間只能做一件事。
所以它使用「事件迴圈」來管理同步與非同步任務:
整個執行流程大致如下:
-
執行 Call Stack(呼叫堆疊)裡的同步程式碼
這是立即執行的部分,例如變數宣告、函式呼叫。 -
當有非同步任務(像 setTimeout、fetch)時,
它們會交由瀏覽器或 Node.js 的「背景系統」處理。 -
背景系統完成後,會把對應的回呼函式(callback)
加入到任務佇列中。 -
事件迴圈檢查:
-
如果 Call Stack 空了(表示同步任務都執行完),
-
就會從任務佇列中取出下一個任務,放進 Call Stack 執行。
-
這個循環重複進行,形成「事件迴圈」。
二、重點:任務加入任務佇列這個動作,不能發生在事件迴圈裡面
因為——
👉 事件迴圈的角色是「調度(scheduling)」而不是「執行(executing)」任務。
換句話說,事件迴圈只是負責:
-
檢查哪裡有可執行的任務(stack 空了嗎?queue 有東西嗎?)
-
從任務佇列取出任務,交回 JS 引擎執行
它不會主動產生任務或把任務加入佇列。
「加入任務佇列」這件事是由**外部環境(如瀏覽器 API 或 Node.js runtime)**完成的。
三、舉個例子說明:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");
執行順序如下:
| 階段 | 動作 | 負責者 |
|---|---|---|
| 1 | console.log("1") 直接執行 | JS 主執行緒 |
| 2 | setTimeout 交給瀏覽器的「Timer 模組」去等待 | 瀏覽器環境 |
| 3 | console.log("3") 執行完畢 | JS 主執行緒 |
| 4 | Timer 模組倒數結束,通知事件系統:可以把 callback 放進任務佇列 | 瀏覽器系統層(非事件迴圈本身) |
| 5 | 事件迴圈發現 call stack 空了,從任務佇列拿 callback 執行 | JS 主執行緒透過事件迴圈調度 |
你可以看到:
-
「把任務放進任務佇列」是由「Timer 模組」完成的,
-
而不是事件迴圈。
四、如果讓「加入任務佇列」發生在事件迴圈內,會出現什麼問題?
假設事件迴圈在運作時同時負責「加入任務佇列」,
那麼它就會在執行中不斷地中斷自己來插入新任務,導致:
-
無限遞迴(infinite loop)或 stack overflow
-
無法保證任務順序
-
整個事件調度機制崩壞
這樣事件迴圈就會「忙於管理自己」而無法正確處理任務了。
五、總結一句話 🌟
「任務佇列的新增,是由外部系統(例如瀏覽器 API 或 Node.js runtime)在事件迴圈之外完成的;事件迴圈只負責檢查佇列與執行任務,而不負責把任務放進去。」
✅ 關鍵概念回顧:
| 名稱 | 誰負責 | 職責 |
|---|---|---|
| Call Stack | JS 引擎 | 執行同步程式 |
| Web APIs / Node APIs | 瀏覽器或 Node.js runtime | 處理非同步任務、把結果放進任務佇列 |
| Task Queue / Microtask Queue | JS runtime | 儲存待執行的任務 |
| Event Loop | JS runtime | 檢查 stack 狀態、調度佇列任務 |