JavaScript - Event Loop
開始之前
- JavaScript 引擎是 單執行緒 ( Single Thread ),也就是 一次只做一件事 / 一次只執行一段程式碼。
- 瀏覽器不只有 JS 引擎,也就是整個瀏覽器並不是只由 JS 引擎組成,而一個分頁 ( Tab ) 僅存在一個 JS 引擎運行 JS 程式。
- 瀏覽器是屬於多程式多執行緒,因此可以在瀏覽器中做出非同步行為。
- Event Loop 顧名思義,就是一個 Loop ( 循環 ),會持續檢查執行堆疊 ( Call Stack ) 有沒有 Callback 可以呼叫。
JavaScript Runtime ( JavaScript 執行環境 )
V8 Engine
Stack 是一個先進後出 ( First In Last Out ) 的運作模式,當程式呼叫函式 ( Call function ) 時會把函式放進 Stack 中,直到函式執行結束 return 後再將函式從 Stack 中取出 ( 清空 )。
範例 :
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()
以上的程式碼執行後在 Call Stack 是這樣運作的 :
(來源 : https://flaviocopes.com/node-event-loop/)以瀏覽器來說,之所以能夠同時做事的原因是,瀏覽器不只有 Runtime 而已。
Web APIs
Web API 範例 :
console.log('Sync 1')
setTimeout(function asyncTimer() {
console.log(`Hey, I'm done.`)
}, 0)
console.log('Sync 2')
執行結果
Sync 1
Sync 2
Hey, I'm done.
Blocking 阻塞
執行堆疊 ( Call Stack ) 清空時,瀏覽器會做兩件事情 :
- 若 Macrotask Queue ( 宏任務佇列 ) 或 Microtask Queue ( 微任務佇列 ) 有任務存在,會讀取其任務 ( 也就是 Event Loop ) 在 Call Stack 上執行完畢並清除。
- Render ( 渲染畫面 ),瀏覽器理想上會進行最快速的重繪頻率 ( 顯示器更新頻率 ) 。
當 Call Stack 還沒有清空之前,瀏覽器是不會重新渲染畫面, 因此若 Call Stack 上的 Callback 函式執行過久,就會產生所謂的 阻塞。
瀏覽器上模擬阻塞 :
const delay = 5000
const end = Date.now() + delay
console.log('blocking start')
while (Date.now() < end) {}
console.log('blocking end')
Event Loop
宏任務 Macrotask
包含的方法 :
- 前面提到的 Web APIs
- I/O ( 讀寫、存取 )
- 載入 JS 檔案並且執行時,例如
<script>
- 渲染畫面 ( Render )
微任務 Microtask
包含的方法 :
- Promise
- MutationObserver ( 監聽 DOM tree 變動的 API )
- Process.nextTick ( 屬於 Node.js 的 Event Loop )
注意這邊的微任務 ( Microtask ),指的是 已經回應後的結果,而且正等待執行中。 例如 : Promise 進行的 Ajax 請求已經確認執行完畢 ( Settled ),此時 .then( ... ) 或 .catch( ... ) 將會進入微任務佇列 ( Microtask Queue ),等待執行。
每當 宏任務 執行完畢時,會檢查有無 微任務 ,若有則執行微任務,當所有微任務執行完畢,就會進行一次畫面渲染 ( Render Task ),渲染 ( Render ) 的動作本身也是一個 宏任務。
範例 :
setTimeout(() => console.log('Timer'))
Promise.resolve().then(() => console.log('Promise'))
console.log('Sync Console')
執行結果
Sync Console
Promise
Timer
當頁面載入,或者在 devtools console 執行,都相當於執行 <script>
,而執行<script>
就等於執行一個 宏任務 ( Macrotask )
當微任務執行完畢後,才會渲染畫面
整理上述,渲染畫面有幾個時機 :
- 當 Call Stack 的 Callback 執行完畢被清空時。
- 當宏任務 ( Macrotask ) 在 Call Stack 執行完畢被清空,且沒有微任務 ( Mircotask ) 時。
- 若有微任務 ( Microtask ),當微任務在 Call Stack 執行完畢時被清空時。
前端面試遇到面試官提問 Event Loop 是什麼該如何回答 ?
JavaScript 引擎是單執行緒,也就是同一時間、一次只做一件事,但瀏覽器中不只有 JavaScript 引擎,還有 Web APIs、GUI、Plugin 等處理程序提供我們非同步操作。
當非同步操作有執行的結果,會被放到 Event Queue,等待 JS 引擎中 Call Stack 同步函式執行完畢後,被放進 Call Stack 接續執行。這種監控 Event Queue 並且將任務放進 Call Stack 執行的行為,就是瀏覽器協調事件的機制 Event Loop。
Reference
- MDN - The event loop
- MDN - In depth: Microtasks and the JavaScript runtime environment
- Loupe
- Youtube - 所以說 event loop 到底是什麼玩意兒?| Philip Roberts | JSConf EU
- JavaScript Visualizer 9000
- JS 原力覺醒 Day13 - Event Queue & Event Loop 、Event Table
- JS 原力覺醒 Day14 - 一生懸命的約定:Promise
- JS 原力覺醒 Day15 - Macrotask 與 MicroTask
- 【JavaScript 筆記】所以事件循環 Event Loop 到底是什麼?
- [JavaScript] Javascript 的事件循環 (Event Loop)、事件佇列 (Event Queue)、事件堆疊 (Call Stack):排隊
- JavaScript 中的同步與非同步(上):先成為 callback 大師吧!
- 重學瀏覽器(1) - 多程式多執行緒的瀏覽器
- 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理
- [Javascript] 深入了解事件迴圈(Event Loop),Macrotask 跟 Microtask 是什麼?
- Youtube - JavaScript 宏任务与微任务 - Web 前端工程师面试题讲解