---
title: Event Loop Note
tags: Event Loop, JavaScript
description: A brief introduction to event loop in Javascript.
---
# JavaScript中的事件循環 (The Event Loop in JavaScript)
:::warning
:bulb: 本站筆記已同步更新到我的[個人網站](https://simplydevs.netlify.app/)囉! 歡迎參觀與閱讀,體驗不同的視覺感受!
:::
[toC]
## [MDN文件](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)中對Event loop的定義
JavaScript中,並行模型(concurrency model)建立於事件迴圈(event loop)的基礎上。Event loop的功能在於執行程式碼、蒐集與處理事件、以及執行等待中的次任務(sub-tasks).
## JavaScript is primarily single threaded.
**JavaScript是一種單線執行(single threaded)的程式語言**,亦即一次只有一條線執行所有的事。例如,有一個`for`迴圈需要花一段時間執行,則必須**等待這個`for`迴圈執行完畢,才能繼續執行後面的程式碼,這就會造成阻塞(blocking)**。
+ **如果是同步(Synchronous)執行:**
想像一間餐廳裡只有一位服務生(單線),假如第一桌的客人舉手找服務生點餐,服務生移動到第一桌,待第一桌點餐完畢,第二桌的客人舉手,服務生才能移動到第二桌;當服務生在第一桌點餐時,第一桌的客人問他的朋友想點甚麼,服務生並不能在此時離開去服務其他桌的客人,必須等第一桌的客人與朋友都決定好了,點餐才算完成。執行程式碼若遇到這種情況,需要等待該片段的code都執行完畢才能往下執行,可能會遇到畫面「卡住」的情況,稱為阻塞(blocking)。
::: info
舉例來說,執行以下程式碼:
```javascript=
console.log("How long will it take")
for (let idx=0; idx < 999999999; idx++) {console.log(`This is the ${idx+1} loop.`)}
console.log("to print the result?")
```
1. Console會先印出'How long will it take'
2. 接著`for`迴圈跑了999999999個迴圈,造成阻塞,因此有一段時間停滯。並且因為`console.log()` 是 I/O 操作,會佔用大量記憶體,甚至可能會拖垮開發工具。
3. 999999999迴圈都跑完後,才印出"to print the result?"
:::
+ **如果是非同步(Asynchronous)執行:**
在餐廳的例子中,假設第一桌的客人點餐需要和朋友討論,服務生可以先去執行其他任務,例如先去送其他桌的餐、或告訴廚房要準備的餐等,待第一桌客人討論完再完成點餐。像Ajax(Asynchronous JavaScript and XML)這種以非同步(Asynchronous)執行的方法,就可以避免在等待回應的過程,無法繼續執行其他動作或導致瀏覽器停滯的情況。
>:arrow_forward:關於JavaScript如何執行非同步事件,這篇參考文章有詳細的說明:[談談JavaScript中的asynchronous和event queue](https://pjchender.blogspot.com/2016/01/javascriptasynchronousevent-queue.html)
:::info
重複一下上面的範例,這次使用`setTimeout()`這個方法:
```javascript=
console.log("How long will it take");
setTimeout(() => { console.log("the result?")}, 2000);
console.log("to print");
```
1. Console會先印出'How long will it take'
2. 接著執行`setTimeout()`,開始計時2秒
3. 再執行第三行,印出"to print"
4. 最後,`setTimeout()`計時2秒結束,印出"the result?"
:::

###### 圖片來源:https://tw.alphacamp.co/blog/ajax-asynchronous-request
---
## 事件循環的主要觀念 Main Concepts in Event Loop

<p style="text-align: center;">JavaScript中的事件循環(Event Loop)</p>
###### 圖片來源:https://dev.to/rahulsaha28/javascript-4j1m
簡單來說,**事件循環是一個用於管理code執行的系統**,我們**想用JS非同步(asynchronously)**執行不阻塞的程式碼,這就是事件循環發揮功能的地方了。事件循環的主要元素包含:
### **1. Memory Heap**
以無順序的方式儲存物件的記憶體。當前使用的JavaScript變數和物件等會儲存在heap中。
### **2. Call Stack**
前面提到,JavaScript是以單線的方式執行,所有待執行的程式碼片段會被放在**堆疊(call stack)中執行**。當調用函式時,一個幀(frame)會被加入堆疊中,幀連結了該函數的參數(argument)與heap中的變數。Frame是以<span style="color:red">後進先出(Last in first out,LIFO)</span>的順序進入堆疊中,可以想成`array.push()`和`array.pop()`:
+ `array.push()`將元素加在array的最後
+ `array.pop()`將最後一個元素移除,並回傳被移除的元素
(參考資料:[【在廚房想30天的演算法】Day 08 資料結構:堆疊 Stack](https://ithelp.ithome.com.tw/articles/10270257))
:::success
例如以下程式碼:
```javascript=1
function one() {
return function two() {
return function three() {
return 'Done!'
}
}
}
console.log(one()()())
```
- **分析程式碼:**
`console.log(one()()())` 其實是:
```javascript=
const result = one(); // result 現在是 function two
const result2 = result(); // result2 現在是 function three
const finalResult = result2(); // finalResult = 'Done!'
console.log(finalResult);
```
所以,函式執行的順序是: 1️⃣ `one()` 2️⃣ `two()` 3️⃣ `three()`
- **Call Stack 進入先後順序:**
當執行 `console.log(one()()())`; 時,Call Stack 變化如下:
1️⃣ `console.log()` 進入 Call Stack(但要等 `one()()()`執行完才輸出)
2️⃣ `one()` 進入 Call Stack
3️⃣ `one()` 回傳 two,從 Call Stack 彈出
4️⃣ `two()` 進入 Call Stack
5️⃣ `two()` 回傳 three,從 Call Stack 彈出
6️⃣ `three()` 進入 Call Stack
7️⃣ `three()` 回傳 'Done!',從 Call Stack 彈出
8️⃣ `console.log('Done!')` 執行並輸出 'Done!',然後從 Call Stack 彈出
- **Call Stack 變化(LIFO - 後進先出)**
```javascript=
// 進入時
[console.log] ⬅️ 1️⃣ 進入
[one] ⬅️ 2️⃣ 進入
[two] ⬅️ 3️⃣ 進入
[three] ⬅️ 4️⃣ 進入
// 彈出時
[three] ⬅️ 5️⃣ 彈出(回傳 'Done!')
[two] ⬅️ 6️⃣ 彈出
[one] ⬅️ 7️⃣ 彈出
[console.log] ⬅️ 8️⃣ 彈出(輸出 'Done!')
```
- **最終執行順序**
函式執行順序(進入 Call Stack 順序): 1️⃣ `one()` 2️⃣ `two()` 3️⃣ `three()`
函式結束順序(從 Call Stack 彈出順序): 1️⃣ `three()` 2️⃣ `two()` 3️⃣ `one()` 4️⃣ `console.log()`(最後輸出 'Done!')
- **總結**:
進入Call Stack的先後順序:
(底部-->頂部)
`global()` :arrow_right: `console.log(function)` :arrow_right: `one()` :arrow_right: `two()` :arrow_right: `three()`
:dart: 第一次啟動程式時,全域執行環境(Global Execution Context) 會被加到call stack中,其中包含全域變數(global variable)和詞彙環境(Lexical Environment)。詞彙環境是指程式碼在程式中的位置,可以參考[這篇文章](https://javascript.plainenglish.io/scope-chain-and-lexical-environment-in-javascript-eb1f6e60997e)。
:::
在JavaScript執行程式碼時,會由上而下、優先從全域(global)的程式碼開始執行,若全域的程式碼需要進入某個函式,再執行該函式,並把此函式加入stack的最上方,接著執行到函式中的return,函式便會移出堆疊(pop off),以下程式碼舉例:
```javascript=
function multiply(a, b) { -multiply函式放入stack最上方
return a * b
}
function square(n) { , -square函式放入stack最上方,呼叫multiply函式
return multiply(n, n)
}
function printSquare(n) { -printSquare函式放入stack最上方,呼叫square函式
let squared = square(n)
console.log(squared)
}
printSquare(4) -全域,呼叫printSquare函式
```
- **以非同步處理避免阻塞**
以下程式碼為例:
```javascript=
console.log('hi')
setTimeout(function () {
console.log('there')
}, 5000)
console.log('JSConfEU')
```
由上而下、先執行全域的`console.log('hi')`(印出hi),接著執行`setTimeout`函式(開始計時五秒),接著執行`console.log('JSConfEU')` (印出JSConfEU),接著五秒倒數歸零,印出there。
:arrow_right: <span style="background-color: gray;"> 所以console印出的順序會是:hi --> JSConfEU --> (5秒後)there </span>
### **3. Event Queue** (或稱Callback Queue)
**非同步程式碼(如 setTimeout, Promise 等)在執行時會將回調函式(callback function)放入 Callback Queue**。
當非同步操作完成後,回調函式並不會立即執行,而是進入 Callback Queue 等待執行。
前文中的例子顯示,在瀏覽器中可以同時處理多個事情,因為瀏覽器不是只有一個JavaScript Runtime(執行環境),如前文中的`setTimeout`,是**瀏覽器提供的一個API(Web API)**,而非JavaScript engine本身的功能。
>:arrow_forward:關於執行環境(runtime),這篇參考文章中有更詳細的說明:[從「為什麼不能用這個函式」談執行環境(runtime)](https://blog.huli.tw/2022/02/09/javascript-runtime/)
:arrow_forward: Web API: 如DOM、ajax、setTimeout、HTTP Request等等,可以幫助單線處理的JavaScript在瀏覽器中完成更多事。
更多WebAPIs可參考[MDN文件](https://developer.mozilla.org/en-US/docs/Web/API)。
`setTimeout` 中的 callback function(簡稱 cb)會被放到 WebAPIs 中,這時候,`setTimeout`已經執行結束,並從call stack中脫離。當計時時間到,就會將cb放入佇列(queue)中。**queue是以<span style="color:red">先進先出(First in first out,FIFO)</span>的方式執行,等待call stack中的任務清空,由事件循環監控,將queue中的任務傳入stack中依序執行**。
因此,簡單的說,**queue就像此單線的to-do-list,存放等待被執行的事件**,<span style="background-color: gray">因此,重複檢查queue中是否有任何需要被執行的事件,若有,則待call stack清空後,將佇列中的事件傳入call stack,直到queue為空;這樣**重複監控call stack與queue的循環,稱為事件循環(Event Loop)**。</span>
#### **範例**
```javascript=
console.log('Start'); // 進入 Call Stack -> 輸出 'Start'
setTimeout(function() {
console.log('Delayed'); // 進入 Callback Queue,等待 2 秒
}, 2000);
console.log('End'); // 進入 Call Stack -> 輸出 'End'
// 2 秒後,'Delayed' 會進入 Callback Queue,等待 Call Stack 清空後執行
```
輸出順序:
```javascript=
Start
End
Delayed
```
#### **setTimeout 0 的意義**
```javascript=
console.log('hi')
setTimeout(function () {
console.log('there')
}, 0)
console.log('JSConfEU')
```
回到前文中的範例程式碼,假如把程式碼中setTimeout等候時間改為0,瀏覽器同樣先執行`console.log('hi')`,遇到setTimeout時間為0,將setTimeout的cb放入WebAPI的計時器中,等時間到再把此cb放入佇列(queue),然而堆疊中任務尚未執行完畢,因此要等`console.log('JSConfEU')`也執行完畢,最後才會印出there。
:arrow_right: <span style="background-color: gray;"> 所以console印出的順序會是:hi --> JSConfEU --> there </span>
setTimeout 的等待時間非執行的保證時間,而是要求執行環境處理所需的最少等待時間 <span style="color: crimson;">**(因此,不管設定時間是0秒或5秒,都必須等待call stack清空才可以將queue中的cb放入call stack執行)**。</span>
如MDN文件中的例子:
```javascript=
(function() {
console.log('this is the start')
setTimeout(function cb() {
console.log('this is a msg from call back')
})
console.log('this is just a message')
setTimeout(function cb1() {
console.log('this is a msg from call back1')
}, 0)
console.log('this is the end')
})()
// Console印出的順序如下:
// "this is the start"
// "this is just a message"
// "this is the end"
// "this is a msg from call back"
// "this is a msg from call back1"
```
## 小結
:pencil: **最後,再用一個例子複習事件循環的過程:**
```javascript=
console.log("First line")
function usingsetTimeout() {
console.log("queue")
}
setTimeout(usingsetTimeout, 3000)
console.log("Last line")
```
1. 首先,`console.log("First line")`被加入<span style="color:Crimson" >**call stack**</span>並執行,印出'First line',接著被移出<span style="color:Crimson" >**call stack**</span>
2. `setTimeout()`被加入<span style="color:Crimson" >**call stack**</span>
3. `setTimeout()`是非同步的,執行後,它會設定計時器,當計時器倒數至 0 後,`setTimeout` 函式本身會從<span style="color:Crimson" >**call stack**</span>中彈出。
> Note:計時期間,`usingsetTimeout` 並未進入<span style="color:ForestGreen" >**Event Queue**</span>,而是計時器歸零後,`usingsetTimeout` 才會被放入<span style="color:ForestGreen" >**Event Queue**</span>等待執行。
4. 同時,事件循環也不斷檢查<span style="color:Crimson" >**call stack**</span>是否都被執行完畢,當 <span style="color:Crimson" >**call stack**</span> 為空時,它才會從 <span style="color:ForestGreen" >**Event Queue**</span> 取出callback函式並執行。
5. `console.log("Last line")`被加入<span style="color:Crimson" >**call stack**</span>並執行,印出'Last line',接著被移出<span style="color:Crimson" >**call stack**</span>
6. 等待 3000 毫秒:
在等待期間,`setTimeout`的callback函式,也就是`usingsetTimeout`並未進入<span style="color:ForestGreen" >**Event Queue**</span>中,它只是在等待計時器倒數結束。
7. 3000 毫秒後,計時器歸零,`usingsetTimeout` 被放入<span style="color:ForestGreen" >**Event Queue**</span>,等待執行。
8. 事件循環發現 <span style="color:Crimson" >**call stack**</span>現在空了,因此把<span style="color:ForestGreen" >**Event Queue**</span>中排在最前面的`usingsetTimeout`推進<span style="color:Crimson" >**call stack**</span>。 <span style="color:ForestGreen" >**Event Queue**</span> 進入 <span style="color:Crimson" >**call stack**</span> 的函式,仍然按照「先進先出(FIFO)」的順序執行。
9. `usingsetTimeout` 執行後,`console.log("queue")`被加入<span style="color:Crimson" >**call stack**</span>並執行,印出'queue',然後被移出<span style="color:Crimson" >**call stack**</span>(遵守call stack後進先出LIFO的順序)。
10. 最後,`usingsetTimeout`被移出<span style="color:Crimson" >**call stack**</span>。
:::success
:bulb: **In a nutshell:**
JavaScript 的 Event Loop(事件迴圈)是其非同步運作的核心機制,負責處理同步與非同步程式碼的執行順序,確保 JavaScript 仍然是單執行緒(single-threaded)但可以處理非同步操作(如 I/O、計時器、DOM 事件等)。
:::
---
## 參考資料
* [並行模型和事件循環(MDN Web Docs)](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/EventLoop#%E5%9F%B7%E8%A1%8C%E7%92%B0%E5%A2%83%E6%A6%82%E5%BF%B5%EF%BC%88runtime_concepts%EF%BC%89)
* [JavaScript - Event Loop](https://ithelp.ithome.com.tw/articles/10230871)
* [【JavaScript筆記】所以事件循環Event Loop到底是什麼?setTimeout 0 的藝術 ─ 我OK、你先請?](https://emilywalkdone.blogspot.com/2021/01/JavaScript-EVENT-LOOP.html)
* [理解JavaScript中的事件循環](https://pjchender.blogspot.com/2017/08/javascript-learn-event-loop-stack-queue.html)
* [What the heck is the event loop anyway? | Philip Roberts | JSConf EU](https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=838s)
* [什麼是 Ajax? 搞懂非同步請求 (Async request) 概念](https://tw.alphacamp.co/blog/ajax-asynchronous-request)
* [【筆記】搞懂 setTimeout 與 Event Loop 事件循環的關係](https://medium.com/@z88243310/%E7%AD%86%E8%A8%98-%E6%90%9E%E6%87%82-settimeout-%E8%88%87-event-loop-%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%92%B0%E7%9A%84%E9%97%9C%E4%BF%82-5f9fc5e5774)
* [Understanding JavaScript — Heap, Stack, Event-loops and Callback Queue](https://javascript.plainenglish.io/understanding-javascript-heap-stack-event-loops-and-callback-queue-6fdec3cfe32e)
* [設計看JS - 設計永遠搞不懂的同步、非同步 02](https://ithelp.ithome.com.tw/articles/10245127)
* [How dose JavaScript work?](https://june2.github.io/posts/Javasciprt/)
* Codecademy教材
::: success
:crescent_moon: 本站內容僅為個人學習記錄,如有錯誤歡迎留言告知、交流討論!
:::