JavaScript Event-Loop 事件循環 (基礎)

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

cover image


前言

這一篇主要是紀錄一下 JavaScriptEvent-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()):

  1. 執行到 bar(),把它放進 Call Stack 裡面
  2. 執行到 foo(),把它放進 Call Stack 裡面
  3. foo() 執行完之後,把它從 Call Stack 裡面移除
  4. bar() 執行完之後,把它從 Call Stack 裡面移除
  5. 程式結束

Call StackEvent-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')

一樣補個圖:

  1. 一開始執行到 setTimeout,把他丟到 Web API 裡面 (雖然是這樣說,不過他還是會先出現在 Call Stack 裡面,然後立刻丟到 Web API 再交由他去處理)
  2. 執行到 console.log('bar'),把他放進 Call Stack 裡面,此時 Web API 裡面的 setTimeout 應該已經執行完了,所以也把他丟到 Queue 裡面
  3. console.log('bar') 執行完之後,把他從 Call Stack 裡面移除
  4. Event-Loop 檢查 Call Stack 是否為空,如果是的話就會把 Queue 裡面的東西放到 Call Stack 裡面執行,所以把 setTimeoutcallback 放到 Call Stack 裡面執行

Web API?

前面提到 JavaScript 是單執行緒的語言,但是 Web API 可以讓你做一些非同步的事情,例如你真的想要在瀏覽器的背景跑,可以依賴的 Web API 就是 Web Worker,他可以讓你在背景跑,而不會影響到你的主執行緒。

Web API 就會去依賴於你的執行環境,例如 Node.jsBrowser 等等,如果在 Browser 可以想像都在你的 window 裡面,而 Node.js 則是在 global 裡面。


Microtask Queue

到最後還有一個東西,叫做 Microtask Queue,他的優先權比 Queue 還要高,所以他會先執行 Microtask Queue 裡面的東西,然後才會執行 Queue 裡面的東西。

最簡單的分辨方式就是 Web API 幫你做的事情,例如 setTimeoutsetInterval 等等,都會被放到 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

他的運作過程是:

  1. setTimeout 會被放到 Queue 裡面
  2. Promise.resolve().then() 會被放到 Microtask Queue 裡面
  3. Promise.resolve().then() 會被放到 Microtask Queue 裡面
  4. console.log('main') 會被放到 Call Stack 裡面執行
  5. Call Stack 為空,Microtask Queue 裡面有東西,所以把 Promise.resolve().then() 放到 Call Stack 裡面執行
  6. Call Stack 為空,Microtask Queue 裡面有東西,所以把 Promise.resolve().then() 放到 Call Stack 裡面執行
  7. Call Stack 為空,Microtask Queue 為空,Queue 裡面有東西,所以把 setTimeout 放到 Call Stack 裡面執行
  8. Call Stack 為空,Microtask Queue 為空,Queue 為空,程式結束

大概是這種感覺。

簡單來說就是:

  1. Call Stack 為空的時候,會先執行 Microtask Queue 裡面的東西
  2. Microtask Queue 為空的時候,會看 Queue 裡面有沒有東西,如果有的話就會執行 Queue 裡面的東西

再來一樣附個圖(省略一些步驟,只留下重點的部分):

有點混亂的話可以到下面這個地方練習,相信你會抓到感覺的(?)

https://laijunbin.github.io/event-loop-practice/


結論

還有 ES7async/await,雖然這篇沒有提到,不過其實可以把他想成就是 Promise 的語法糖,所以他也會被放到 Microtask Queue 裡面;或是像其他 Web API,例如 requestAnimationFramerequestIdleCallback 等等沒有提到,所以這篇只是一個基礎的概念,但總是要把基礎打好。

細節可以到 https://laijunbin.github.io/event-loop-practice/ 練習看看,相信你會抓到感覺的。 (不包含 requestAnimationFrame 那些,因為他不保證輸出的順序,加進去其實意義不大)

覺得好用,有幫助到你的話,可以幫我按個 star




最後更新時間: 2023年08月11日.