我在面試我現在的工作的時候,被問了 Event loop 是如何運作的。當時的程度,其實連 Event loop 這個名詞都不知道,就是含糊地把 JavaScript 如何處理事件優先順序、同步與非同步 task 的過程描述了一遍。(最後還錄取了?)
在這篇文章裡將會由淺入深,好好的把 Event loop 如何運作講一遍。希望在學習 JS 的人可以一次就把重點全都打包,不要像我面試全憑運氣。
首先我們要知道 JavaScript 是**單執行緒(single-threaded)**語言,它透過 Event loop 管理同步與非同步任務。
這條主線程負責執行所有同步程式碼,並且透過 call stack 依序處理函式的進入與返回,當程式中遇到非同步任務,例如計時器、網路請求或 Promise,這些任務的 callback function 不會立刻進入主線程,而是被安排到對應的任務佇列(queue)中等待。
任務佇列分成兩種:
Promise.then
、await
後的程式碼、queueMicrotask
、MutationObserver
等setTimeout
、setInterval
、I/O、DOM event callback
事件循環的核心機制就是不斷檢查主線程是否空閒,當 call stack 清空時,它會先取出所有微任務逐一執行,然後才會從宏任務佇列中取出下一個宏任務進入主線程執行,如此往復形成一個循環的機制。
這種架構確保了 JavaScript 雖然是單執行緒,但依然能夠有效率地處理大量非同步操作,並維持程式執行的可預測順序。
宏任務是事件循環中每輪(tick)執行的主要任務,顧名思義,「宏」代表一個大的工作單位,完成後會進入下一輪事件循環。
常見來源
宏任務來源 | 說明 |
---|---|
setTimeout |
延遲執行 |
setInterval |
定時重複執行 |
setImmediate (Node) |
下一輪立即執行 |
I/O(如讀檔) | 讀寫操作回調 |
UI事件(click等) | 瀏覽器事件回調 |
微任務是在每個宏任務結束後立即清空,不等待下一輪事件循環,以確保它們非常快被處理,比宏任務優先。
常見來源
微任務來源 | 說明 |
---|---|
Promise.then() |
Promise 回調 |
catch() / finally() |
同上 |
queueMicrotask() |
明確加入微任務隊列 |
MutationObserver |
監測 DOM 變化 |
process.nextTick() (Node) |
特例,比其他微任務更早執行 |
我們第一天就提到了 EC 代表 call stack 中一塊塊執行中的程式碼,包含變數、函式的作用域跟 this。當時都只提到同步的程式碼(可以想成呼叫 function 的當下會馬上執行)。Event Loop 負責協調同步程式執行和異步操作,它的工作機制是:
打比方的話,Call stack像一個工作桌,只能放一件事,做完一件事才放下一件;Task queue 像待辦清單,由 Event Loop 檢查有無新任務,依順序排進桌上。
第一個範例小試身手,非常簡單,輸出為
1 3 2
因為同步的程式會依序跑,然後才進行異步的 callback。
接下來稍微複雜一點,有同步的、微任務與宏任務。
提示:
promise
是微任務。
分析流程
步驟 | 執行內容 | 類型 | 加入哪個佇列? |
---|---|---|---|
1 | console.log('script start') | 同步 | 立即執行 |
2 | setTimeout 註冊 | 宏任務 | 加入宏任務 queue |
3 | Promise.then() | 微任務 | 加入微任務 queue |
4 | console.log('script end') | 同步 | 立即執行 |
事件循環流程
因此,輸出結果為
script start
script end
promise1
promise2
setTimeout
分析流程
步驟 | 執行內容 | 類型 | 加入佇列 |
---|---|---|---|
1 | console.log('script start') | 同步 | - |
2 | setTimeout 註冊 | 宏任務 | 加入宏任務 queue |
3 | 呼叫 asyncFunc | 同步部分 | - |
4 | await Promise | 微任務 | 加入微任務 queue |
5 | Promise.then().then() | 微任務 | 加入微任務 queue |
6 | console.log('script end') | 同步 | - |
微任務執行順序
宏任務執行順序
最終輸出順序
script start
asyncFunc start
script end
promise1
promise2
asyncFunc after await
setTimeout
這邊我放兩段程式碼,有興趣的讀者可以挑戰判斷看看輸出的結果是什麼,明天公布解答~
(1) 簡單
(2) 稍微複雜
任務類型 | 加入時機 | 清空時機 |
---|---|---|
微任務 | 立即執行後加入 | 每輪宏任務結束後全部清空 |
宏任務 | setTimeout、setInterval、DOM事件 | 逐一執行,之後清空微任務 |
事件循環流程: