Try   HackMD

JavaScript中的事件循環 (The Event Loop in JavaScript)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
本站筆記已同步更新到我的個人網站囉! 歡迎參觀與閱讀,體驗不同的視覺感受!

MDN文件中對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)。

舉例來說,執行以下程式碼:

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)執行的方法,就可以避免在等待回應的過程,無法繼續執行其他動作或導致瀏覽器停滯的情況。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
關於JavaScript如何執行非同步事件,這篇參考文章有詳細的說明:談談JavaScript中的asynchronous和event queue

重複一下上面的範例,這次使用setTimeout()這個方法:

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?"

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

圖片來源:https://tw.alphacamp.co/blog/ajax-asynchronous-request

事件循環的主要觀念 Main Concepts in Event Loop

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

JavaScript中的事件循環(Event Loop)

圖片來源:https://dev.to/rahulsaha28/javascript-4j1m

簡單來說,事件循環是一個用於管理code執行的系統,我們**想用JS非同步(asynchronously)**執行不阻塞的程式碼,這就是事件循環發揮功能的地方了。事件循環的主要元素包含:

1. Memory Heap

以無順序的方式儲存物件的記憶體。當前使用的JavaScript變數和物件等會儲存在heap中。

2. Call Stack

前面提到,JavaScript是以單線的方式執行,所有待執行的程式碼片段會被放在堆疊(call stack)中執行。當調用函式時,一個幀(frame)會被加入堆疊中,幀連結了該函數的參數(argument)與heap中的變數。Frame是以後進先出(Last in first out,LIFO)的順序進入堆疊中,可以想成array.push()array.pop():

例如以下程式碼:

​​ function one() { ​​ return function two() { ​​ return function three() { ​​ return 'Done!' ​​ } ​​ } ​​ } console.log(one()()())
  • 分析程式碼:
    console.log(one()()()) 其實是:
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 - 後進先出)
// 進入時 [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()

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    console.log(function)
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    one()
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    two()
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    three()

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
第一次啟動程式時,全域執行環境(Global Execution Context) 會被加到call stack中,其中包含全域變數(global variable)和詞彙環境(Lexical Environment)。詞彙環境是指程式碼在程式中的位置,可以參考這篇文章

在JavaScript執行程式碼時,會由上而下、優先從全域(global)的程式碼開始執行,若全域的程式碼需要進入某個函式,再執行該函式,並把此函式加入stack的最上方,接著執行到函式中的return,函式便會移出堆疊(pop off),以下程式碼舉例:

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函式
  • 以非同步處理避免阻塞
    以下程式碼為例:
console.log('hi') setTimeout(function () { console.log('there') }, 5000) console.log('JSConfEU')

由上而下、先執行全域的console.log('hi')(印出hi),接著執行setTimeout函式(開始計時五秒),接著執行console.log('JSConfEU') (印出JSConfEU),接著五秒倒數歸零,印出there。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
所以console印出的順序會是:hi > JSConfEU > (5秒後)there

3. Event Queue (或稱Callback Queue)

非同步程式碼(如 setTimeout, Promise 等)在執行時會將回調函式(callback function)放入 Callback Queue

當非同步操作完成後,回調函式並不會立即執行,而是進入 Callback Queue 等待執行。

前文中的例子顯示,在瀏覽器中可以同時處理多個事情,因為瀏覽器不是只有一個JavaScript Runtime(執行環境),如前文中的setTimeout,是瀏覽器提供的一個API(Web API),而非JavaScript engine本身的功能。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
關於執行環境(runtime),這篇參考文章中有更詳細的說明:從「為什麼不能用這個函式」談執行環境(runtime)
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Web API: 如DOM、ajax、setTimeout、HTTP Request等等,可以幫助單線處理的JavaScript在瀏覽器中完成更多事。
更多WebAPIs可參考MDN文件

setTimeout 中的 callback function(簡稱 cb)會被放到 WebAPIs 中,這時候,setTimeout已經執行結束,並從call stack中脫離。當計時時間到,就會將cb放入佇列(queue)中。queue是以先進先出(First in first out,FIFO)的方式執行,等待call stack中的任務清空,由事件循環監控,將queue中的任務傳入stack中依序執行

因此,簡單的說,queue就像此單線的to-do-list,存放等待被執行的事件因此,重複檢查queue中是否有任何需要被執行的事件,若有,則待call stack清空後,將佇列中的事件傳入call stack,直到queue為空;這樣重複監控call stack與queue的循環,稱為事件循環(Event Loop)

範例

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 清空後執行

輸出順序:

Start End Delayed

setTimeout 0 的意義

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。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
所以console印出的順序會是:hi > JSConfEU > there

setTimeout 的等待時間非執行的保證時間,而是要求執行環境處理所需的最少等待時間 (因此,不管設定時間是0秒或5秒,都必須等待call stack清空才可以將queue中的cb放入call stack執行)

如MDN文件中的例子:

(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"

小結

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
 最後,再用一個例子複習事件循環的過程:

console.log("First line") function usingsetTimeout() { console.log("queue") } setTimeout(usingsetTimeout, 3000) console.log("Last line")
  1. 首先,console.log("First line")被加入call stack並執行,印出'First line',接著被移出call stack

  2. setTimeout()被加入call stack

  3. setTimeout()是非同步的,執行後,它會設定計時器,當計時器倒數至 0 後,setTimeout 函式本身會從call stack中彈出。

Note:計時期間,usingsetTimeout 並未進入Event Queue,而是計時器歸零後,usingsetTimeout 才會被放入Event Queue等待執行。

  1. 同時,事件循環也不斷檢查call stack是否都被執行完畢,當 call stack 為空時,它才會從 Event Queue 取出callback函式並執行。

  2. console.log("Last line")被加入call stack並執行,印出'Last line',接著被移出call stack

  3. 等待 3000 毫秒:
    在等待期間,setTimeout的callback函式,也就是usingsetTimeout並未進入Event Queue中,它只是在等待計時器倒數結束。

  4. 3000 毫秒後,計時器歸零,usingsetTimeout 被放入Event Queue,等待執行。

  5. 事件循環發現 call stack現在空了,因此把Event Queue中排在最前面的usingsetTimeout推進call stackEvent Queue 進入 call stack 的函式,仍然按照「先進先出(FIFO)」的順序執行。

  6. usingsetTimeout 執行後,console.log("queue")被加入call stack並執行,印出'queue',然後被移出call stack(遵守call stack後進先出LIFO的順序)。

  7. 最後,usingsetTimeout被移出call stack

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
In a nutshell:
JavaScript 的 Event Loop(事件迴圈)是其非同步運作的核心機制,負責處理同步與非同步程式碼的執行順序,確保 JavaScript 仍然是單執行緒(single-threaded)但可以處理非同步操作(如 I/O、計時器、DOM 事件等)。


參考資料

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
 本站內容僅為個人學習記錄,如有錯誤歡迎留言告知、交流討論!