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
。