iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
Modern Web

JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天系列 第 4

離開 JS 初階工程師新手村的 Day 04|時間迷宮:事件循環 Event Loop

  • 分享至 

  • xImage
  •  

我在面試我現在的工作的時候,被問了 Event loop 是如何運作的。當時的程度,其實連 Event loop 這個名詞都不知道,就是含糊地把 JavaScript 如何處理事件優先順序、同步與非同步 task 的過程描述了一遍。(最後還錄取了?)

在這篇文章裡將會由淺入深,好好的把 Event loop 如何運作講一遍。希望在學習 JS 的人可以一次就把重點全都打包,不要像我面試全憑運氣。

基礎架構

首先我們要知道 JavaScript 是**單執行緒(single-threaded)**語言,它透過 Event loop 管理同步與非同步任務。

這條主線程負責執行所有同步程式碼,並且透過 call stack 依序處理函式的進入與返回,當程式中遇到非同步任務,例如計時器、網路請求或 Promise,這些任務的 callback function 不會立刻進入主線程,而是被安排到對應的任務佇列(queue)中等待。

任務佇列分成兩種:

  • 微任務(Microtask Queue)Promise.thenawait後的程式碼、queueMicrotaskMutationObserver
  • 宏任務(Macro-task Queue)setTimeoutsetInterval、I/O、DOM event callback

事件循環的核心機制就是不斷檢查主線程是否空閒,當 call stack 清空時,它會先取出所有微任務逐一執行,然後才會從宏任務佇列中取出下一個宏任務進入主線程執行,如此往復形成一個循環的機制。

這種架構確保了 JavaScript 雖然是單執行緒,但依然能夠有效率地處理大量非同步操作,並維持程式執行的可預測順序。

宏任務(Macro-tasks)

宏任務是事件循環中每輪(tick)執行的主要任務,顧名思義,「宏」代表一個大的工作單位,完成後會進入下一輪事件循環。

常見來源

宏任務來源 說明
setTimeout 延遲執行
setInterval 定時重複執行
setImmediate(Node) 下一輪立即執行
I/O(如讀檔) 讀寫操作回調
UI事件(click等) 瀏覽器事件回調

微任務(Micro-tasks)

微任務是在每個宏任務結束後立即清空,不等待下一輪事件循環,以確保它們非常快被處理,比宏任務優先。

常見來源

微任務來源 說明
Promise.then() Promise 回調
catch() / finally() 同上
queueMicrotask() 明確加入微任務隊列
MutationObserver 監測 DOM 變化
process.nextTick()(Node) 特例,比其他微任務更早執行

Execution Context 與 Event Loop 的關係

我們第一天就提到了 EC 代表 call stack 中一塊塊執行中的程式碼,包含變數、函式的作用域跟 this。當時都只提到同步的程式碼(可以想成呼叫 function 的當下會馬上執行)。Event Loop 負責協調同步程式執行和異步操作,它的工作機制是:

  1. 執行 Call stack 中的同步程式碼
  2. 當 Call stack 為空時,檢查 Callback queue,裡頭裝有微任務與宏任務
  3. 將 Callback queue 中的回調函式推入 Call stack 執行
    3-1. 先檢查微任務
    3-2. 清空後才取一個宏任務放進 Call stack

打比方的話,Call stack像一個工作桌,只能放一件事,做完一件事才放下一件;Task queue 像待辦清單,由 Event Loop 檢查有無新任務,依順序排進桌上。

範例

1. 基本的同步 vs 非同步

https://ithelp.ithome.com.tw/upload/images/20250915/20168365n3ght7hjTX.png

第一個範例小試身手,非常簡單,輸出為

1 3 2

因為同步的程式會依序跑,然後才進行異步的 callback。

接下來稍微複雜一點,有同步的、微任務與宏任務。

提示: promise 是微任務。

https://ithelp.ithome.com.tw/upload/images/20250915/20168365S4QmYLn1QI.png

分析流程

步驟 執行內容 類型 加入哪個佇列?
1 console.log('script start') 同步 立即執行
2 setTimeout 註冊 宏任務 加入宏任務 queue
3 Promise.then() 微任務 加入微任務 queue
4 console.log('script end') 同步 立即執行

事件循環流程

  1. 主線程執行完成(同步部分)→ 清空微任務 queue
    • 執行 promise1 → 輸出:promise1
    • 接著執行 promise2(第二個 then)→ 輸出:promise2
  2. 執行第一個宏任務 queue
    • 執行 setTimeout → 輸出:setTimeout

因此,輸出結果為

script start
script end
promise1
promise2
setTimeout

2. 混合 async/await + Promise + setTimeout

https://ithelp.ithome.com.tw/upload/images/20250915/20168365YvdZnrUF3K.png

分析流程

步驟 執行內容 類型 加入佇列
1 console.log('script start') 同步 -
2 setTimeout 註冊 宏任務 加入宏任務 queue
3 呼叫 asyncFunc 同步部分 -
4 await Promise 微任務 加入微任務 queue
5 Promise.then().then() 微任務 加入微任務 queue
6 console.log('script end') 同步 -

微任務執行順序

  1. promise1
  2. promise2
  3. asyncFunc after await

宏任務執行順序

  1. setTimeout

最終輸出順序

script start
asyncFunc start
script end
promise1
promise2
asyncFunc after await
setTimeout

3. 挑戰多層 Promise 與 await

這邊我放兩段程式碼,有興趣的讀者可以挑戰判斷看看輸出的結果是什麼,明天公布解答~
(1) 簡單
https://ithelp.ithome.com.tw/upload/images/20250915/20168365jqxhvk9qSt.png

(2) 稍微複雜
https://ithelp.ithome.com.tw/upload/images/20250915/20168365BfVTe9BXz2.png

總結

任務類型 加入時機 清空時機
微任務 立即執行後加入 每輪宏任務結束後全部清空
宏任務 setTimeout、setInterval、DOM事件 逐一執行,之後清空微任務

事件循環流程

  • 執行同步程式
  • 清空所有微任務
  • 執行一個宏任務(例如一個 setTimeout)
  • 再清空所有微任務
  • 重複這個流程

上一篇
離開 JS 初階工程師新手村的 Day 03|找到真正的冒險者:this 是誰
系列文
JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言