JavaScript Event-Loop 事件循環 (基礎)
tags: Javascript
category: Front-End
description: JavaScript Event-Loop 事件循環 (基礎)
created_at: 2023/08/11 14:00:00

前言
這一篇主要是紀錄一下 JavaScript 的 Event-Loop 的一些基本概念(?),然後最後提供一個練習的網站,可以讓你自己去玩玩看。
預備知識
在開始之前,先假設你知道 Call Stack 的概念,他會記錄你的程式碼執行到哪裡,然後在執行完之後會把他從 Call Stack 裡面移除。
然後像是函數呼叫也是用到 Call Stack,而遞迴的話因為他會一直呼叫自己,直到滿足某個條件才會停止,所以有時候你的遞迴太深的話,就會出現 Call Stack 滿了的錯誤。
舉個簡單的例子(一般函數呼叫)
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
foo();
}
bar();
而他的輸出則是
bar
foo
為了精簡一些,就先無視掉 console.log 的部分,如果用文字描述會像下面這樣:
你可以想像最原本你的 Call Stack 是空的,然後你呼叫 bar(),所以 bar() 會被放到 Call Stack 裡面,然後 bar() 執行到 foo() 的時候,foo() 也會被放到 Call Stack 裡面,然後 foo() 執行完之後就會被移除,接著 bar() 也會被移除,最後 Call Stack 就會變成空的。
或是你要想像最原始有個 main(),然後 main() 呼叫 bar(),然後 bar() 呼叫 foo(),然後 foo() 執行完之後回到 bar(),然後 bar() 執行完之後回到 main(),最後 main() 執行完之後程式就結束了。
概念是一樣的。
來補個圖(假設沒有 main()):
- 執行到
bar(),把它放進Call Stack裡面
- 執行到
foo(),把它放進Call Stack裡面
foo()執行完之後,把它從Call Stack裡面移除
bar()執行完之後,把它從Call Stack裡面移除
- 程式結束
Call Stack 和 Event-Loop
JavaScript 本身是一個單執行緒的語言,而 Event-Loop 則是讓你以為他是多執行緒的東西(?)。
他們之間有一個 Queue 來溝通,而 Event-Loop 會不斷的檢查 Call Stack 是否為空,如果是的話就會去檢查 Queue 裡面有沒有東西,如果有的話就會把 Queue 裡面的東西放到 Call Stack 裡面執行。
舉個例子
setTimeout(() => {
console.log('foo');
}, 0);
console.log('bar');
輸出則是
bar
foo
上面這個問題很簡單,但是初學者可能會覺得很奇怪,因為你會覺得 setTimeout 的時間是 0,所以應該會先執行 setTimeout,但實際上是先執行 console.log('bar'),然後才會執行 setTimeout。
原因就是因為他碰到 setTimeout 的時候,會把他丟到 Web API 裡面,然後 setTimeout 的時間到了之後,會把他丟到 Queue 裡面,然後 Event-Loop 會檢查 Call Stack 是否為空,如果是的話就會把 Queue 裡面的東西放到 Call Stack 裡面執行,所以才會先執行 console.log('bar')。
一樣補個圖:
- 一開始執行到
setTimeout,把他丟到Web API裡面 (雖然是這樣說,不過他還是會先出現在Call Stack裡面,然後立刻丟到Web API再交由他去處理)
- 執行到
console.log('bar'),把他放進Call Stack裡面,此時Web API裡面的setTimeout應該已經執行完了,所以也把他丟到Queue裡面
console.log('bar')執行完之後,把他從Call Stack裡面移除
Event-Loop檢查Call Stack是否為空,如果是的話就會把Queue裡面的東西放到Call Stack裡面執行,所以把setTimeout的callback放到Call Stack裡面執行
Web API?
前面提到 JavaScript 是單執行緒的語言,但是 Web API 可以讓你做一些非同步的事情,例如你真的想要在瀏覽器的背景跑,可以依賴的 Web API 就是 Web Worker,他可以讓你在背景跑,而不會影響到你的主執行緒。
而 Web API 就會去依賴於你的執行環境,例如 Node.js、Browser 等等,如果在 Browser 可以想像都在你的 window 裡面,而 Node.js 則是在 global 裡面。
Microtask Queue
到最後還有一個東西,叫做 Microtask Queue,他的優先權比 Queue 還要高,所以他會先執行 Microtask Queue 裡面的東西,然後才會執行 Queue 裡面的東西。
最簡單的分辨方式就是 Web API 幫你做的事情,例如 setTimeout、setInterval 等等,都會被放到 Queue 裡面,而 Promise 相關的會被放到 Microtask Queue 裡面。
舉個例子
setTimeout(() => {
console.log('foo');
}, 0);
Promise.resolve().then(() => {
console.log('bar');
});
Promise.resolve().then(() => {
console.log('baz');
});
console.log('main')
輸出則是
main
bar
baz
foo
他的運作過程是:
setTimeout會被放到Queue裡面Promise.resolve().then()會被放到Microtask Queue裡面Promise.resolve().then()會被放到Microtask Queue裡面console.log('main')會被放到Call Stack裡面執行Call Stack為空,Microtask Queue裡面有東西,所以把Promise.resolve().then()放到Call Stack裡面執行Call Stack為空,Microtask Queue裡面有東西,所以把Promise.resolve().then()放到Call Stack裡面執行Call Stack為空,Microtask Queue為空,Queue裡面有東西,所以把setTimeout放到Call Stack裡面執行Call Stack為空,Microtask Queue為空,Queue為空,程式結束
大概是這種感覺。
簡單來說就是:
- 當
Call Stack為空的時候,會先執行Microtask Queue裡面的東西 - 當
Microtask Queue為空的時候,會看Queue裡面有沒有東西,如果有的話就會執行Queue裡面的東西
再來一樣附個圖(省略一些步驟,只留下重點的部分):

有點混亂的話可以到下面這個地方練習,相信你會抓到感覺的(?)
https://laijunbin.github.io/event-loop-practice/
結論
還有 ES7 的 async/await,雖然這篇沒有提到,不過其實可以把他想成就是 Promise 的語法糖,所以他也會被放到 Microtask Queue 裡面;或是像其他 Web API,例如 requestAnimationFrame、requestIdleCallback 等等沒有提到,所以這篇只是一個基礎的概念,但總是要把基礎打好。
細節可以到 https://laijunbin.github.io/event-loop-practice/ 練習看看,相信你會抓到感覺的。 (不包含 requestAnimationFrame 那些,因為他不保證輸出的順序,加進去其實意義不大)
覺得好用,有幫助到你的話,可以幫我按個 star。