JavaScript - Call Stack , Event Queue & Event Loop




JavaScript是同步執行的程式語言

JavaScript 是一個設計為同步執行的程式語言,並且是單執行緒(single-threaded)。單執行緒可以想像成有一個餐廳的員工同時負責櫃檯和廚房的工作,他的能力強大到可以同時在兩份工作之間快速的切換,仿佛有多個員工在做事的錯覺。

竟然 JavaScript 不是非同步執行程式碼,那為什麼可以監聽瀏覽器的一些事件,像是移動滑鼠、點擊按鈕、捲動頁面、資料請求和檔案寫入這類型的非同步呼叫。

其實瀏覽器不是只有 JavaScript Engine 的存在

整個瀏覽器的運行環境並非只由 JS 引擎組成,瀏覽器還包含許多其他的部分,像是渲染引擎(Rendering Engine)和 HTTP 請求(Http Request),而瀏覽器也提供 Web API 讓 JS 做應用,像是操作 DOM 節點 、 AJAX 請求、 setTimeout 計時、 Geolocation 地理位置。雖然 JS 引擎身是同步執行,但透過和瀏覽器的其他引擎互動來達到非同步的效果。

Call Stack

JS 的執行堆疊(call stack)記錄了 function 在 JS engine內的執行順序,意思就是程式目前執行到哪邊, call stack 是以同步的方式由上而下執行,採取後進先出 Last In, First Out (LIFO) ,最先執行的函式會被放在 stack 的最下面,如果在函式中執行了 return ,則會將該函式 pop out 出 stack 。

請參考以下範例:

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}

function bar() {
console.log("-> start [bar]");
foo()
console.log("<- end [bar]");
}

bar();

以下為範例各執行階段:

  1. bar() 會先被執行

  2. bar() 印出 “start”

  3. foo() 執行,此時 bar() 仍然在執行中

  4. foo() 印出 “start”

  5. foo() 印出 “end”

  6. foo() 結束執行

  7. bar() 印出 “end”

  8. bar() 結束執行

Stack Overflow

如果遞迴函式不斷呼叫自己而沒有一個中斷點,則會造成瀏覽器產生 stack error :

Event Queue

前面有說過瀏覽器除了有 JS Engine 外還有許多 Web API 可做應用,這些第三方 API 可以與 JS 一起執行,當遇到需要執行 call back 時,則會使用非同步模式,先將這些函式放到 Web APIs 中,在繼續執行主程式,等到時間到或是事件被觸發的時候,再把 call back 推送到 Event Queue(事件佇列) 中,等到整個 JS 執行環境結束後(Call Stack 清空),才依序呼叫 Event Queue 裡面的函式,採取先進先出 first in, first out (FIFO)。

請參考以下範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}

function bar() {
console.log("-> start [bar]");
console.log("<- end [bar]");
}


function baz() {
console.log("-> start [baz]");

setTimeout(foo, 1000);
setTimeout(bar, 1000);

console.log("<- end [baz]");
}

baz();

以下為範例各執行階段:

  1. baz() 會先被執行

  2. baz() 印出 “start”

  3. foo() 先放到 Web APIs ,等待1秒後移至 Event Queue 中。

  4. bar() 先放到 Web APIs ,等待1秒後移至 Event Queue 中。

  5. baz() 印出 “end”

  6. baz() 執行完成,call stack 清空

  7. 透過 Event Loop 機制選取 queue 中的 foo()。

  8. foo() 印出 “start”

  9. foo() 印出 “end”

  10. foo() 執行完成,call stack 清空

  11. 透過 Event Loop 機制選取 queue 中的 bar()。

  12. bar() 印出 “start”

  13. bar() 印出 “end”

  14. bar() 執行完成,call stack 清空

Event Loop

JS 當事件發生時,並不是馬上執行指定的函式,而是將事件排入 Event queue ,接著會重複檢查 call stack 是不是空的?如果是空的,再去看 Event queue 中有沒有待執行的函式,有的話就將函式 pop out ,放入 call stack 中執行。

參考文獻

  1. 所以說event loop到底是什麼玩意兒?| Philip Roberts | JSConf EU

  2. [JS] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式(Learn event loop, stack, queue, and concurrency mode of JavaScript in depth)

  3. Day06 JS是同步還是非同步?

  4. JS 原力覺醒 Day13 - Event Queue & Event Loop 、Event Table

  5. 非同步設計

  6. Understanding the JavaScript call stack