筆記-JavaScript中的call stack/ callback queue/ event loop

Yunya Hsu
10 min readApr 22, 2022

What is JavaScript?

JavaScript 是一個具有 single-threaded, non-blocking, asynchronous, concurrent 特性的語言。

  • single-threaded:單執行緒,一次只會處理一件事
  • non-blocking:非阻塞,不會因為要等待結果而卡住不動。
  • asynchronous:非同步,意思是「發出要求」到「收到結果」的中間可以去做其他事情,不用等在原地。
  • concurrent:多個進行同步發生。

wait a second,以上四個特性聽起來互斥啊?

single-threaded 要怎麼做到 asynchronous?single-threaded 要如何保證一定 non-blocking ?

但 JavaScript 的確可以達到以上四個特性,在了解它背後的原理之前,我們要先認識一些基本概念和名詞解釋。

Runtime environment of JavaScript

JavaScript Engine

程式語言經由運行模式可以分為兩大類,一種是編譯語言,另一種是直譯語言;這兩種類型語言的共通點在於,都必須將我們人類寫的程式碼(高階語言),轉換成電腦看得懂的機器碼(低階語言)。(這兩類的區別不是今天的重點,所以先跳過,有興趣的人再自己 google)

JavaScript 屬於直譯語言,代表 JavaScript code 無法獨立執行,所以我們需要 JavaScript Engine 來處理編譯、並且執行產生結果的環境,這也就是 JavaScript Engine 的工作。

JavaScript Engine 一般來說會附帶在瀏覽器 (browser) 中,但不限於 browser,例如 V8 engine 也存在在 Node.js 和 Deno runtime systems;除了 V8 engine ,也有其他的 JavaScript Engine 例如 SpiderMonkey 等,但現在最主流的就是 V8。

如下圖,JavaScript 運行時有兩個主要部分(先記著名詞,後面再解釋):

  1. Memory Heap:資料儲存的地方
  2. Call Stack:代碼運行時堆疊的地方
from How JavaScript works: an overview of the engine, the runtime, and the call stack

Web APIs

前面提到 JavaScript Engine 一般來說會附帶在瀏覽器 (browser) 中,而 browser 中通常會有其他 APIs 是我們也會經常用到,例如 DOM/ AJAX/ setTimeout…etc.。

我們常常使用這些 APIs ,但有一個重要的觀念要注意:APIs 是由 browser 提供,而不是 JavaScript engine;這些 APIs 我們只能調用,沒辦法去修改他們,因為他們是由 browser 提供的。

所以,我們在執行程序碼的時候,執行環境 (runtime environment) 其實是包含了 JavaScript engine 和 Web APIs,彙整起來如下圖:

from How JavaScript works: an overview of the engine, the runtime, and the call stack

名詞解釋

在討論 event loop 之前,先喘口氣,因為上面累積了很多我們不太熟悉的名詞,我們先逐一攻破。

前面分別解釋了 JavaScript Engin 和 WebAPIs,所以兩個彙整起來,可以看出 JavaScript 的執行環境(Runtime environment)應該長得像下圖:

from MDN: the event loop

Memory Heap

中文通常翻譯成堆積。

Heap 是一個無結構的大區域(不像 stack 或 queue 有明確的順序),object 被分配在 heap 中。你可以想像成一個巨大的倉庫。

Callback Queue

中文通常翻譯成佇列。

Queue 裡面的資料結構是屬於 FIFO (first in first out),也就是說先被放進去的資料會優先被執行,這應該滿好理解的,就像日常生活的排隊:在火車站買票時,在排隊隊伍中位於第一個的人會先買到票,依次是排第二位、第三位…etc

Queue 裡面放待處理的 message(訊息),每個 message 都與一個 function 相關聯。當 stack 中有足夠空間時,stack 會從 queue 拿取一個 message 進行處理,處理過程包含了呼叫相關聯的 function;只有當 stack 清空時,該 message 才算是完成處理。

Call Stack

這是一個比較複雜的部分,我們分兩個大項介紹:

名詞解釋

中文通常翻譯成堆疊。前面說到 stack 是代碼運行時堆疊的地方,當我們呼叫 function 時,這些 function 會形成一個又一個的 frame,堆疊在 stack 裡面。

與 Queue 相反,這些 frame 堆疊方式是 LIFO (last in first out),如果依序呼叫 push(), push(), pop() 後,是第二個 push 進去的資料會被 pop 出來,而不是第一個。

如果還是很抽象,可以想像成品客洋芋片:最後一片被裝進去的洋芋片,一定是第一片被拿出來的。

但要注意, call stack 並不是無限大,有一個情況叫做 Blowing the stack,意思是「堆積的 frame 已經達到 call stack 的最大值」,也就是堆不下了!當我們沒有寫好邏輯的時候很容易發生這種情況,陷入無限迴圈……

single-threaded 特性與其限制

JavaScript 的 single-threaded (單線程) 特性也在此體現,因為是 call stack,所以 JS 一次只會執行一部分程式,必須等這部分程式「完成」,才會執行接續的程式碼。(可以想像成要吃完第一片洋芋片,才可以吃第二片~)

single-threaded 的好處是我們不需要處理 concurrency issues (併發性問題),應該不難理解,簡單來說就是非單線程容易出現卡住/ 死結的情況;但這不代表 single-threaded 一定比較好,另一個顯而易見的限制就是:大家都要等。

要吃完第一片洋芋片才能吃第二片,但如果今天出現例外情況,像是第一片洋芋片比平底鍋還大呢?

想當然爾,第二個待執行的程式碼就會停下來,等第一個程式碼完成才能繼續;這將造成 blocking ,具體實例就是還未有 ajax 的時代,瀏覽器要等 API 抓完全部資料才會渲染畫面(即便有些畫面根本不需要 API 的資訊),這將造成使用者體驗不佳。

要解決這個問題,我們將搭配 event loop 來處理。

What is event loop?

前面提到的一個重要概念是,JS 並不會「單獨運行」,它總是會運作在某一個環境(hosting environment)裡面,例如瀏覽器、Node.js、或其他的 embedded system。

這些 hosting environment 有一個叫做「事件循環 (event loop)」 的機制,由 event loop 這個機制來主導調用 JavaScript code 何時該執行,也因為如此,我們才可以達成 non-blocking 和 asynchronous 。

聽起來很抽象,我們可以用一個簡單的 ajax 的實例(使用 axios 向 API 獲取資料)來看。

注意一下,裡面的 function (response) 是 callback function。

axios.get('<https://dog.ceo/api/breeds/image/random>')
.then(function (response) {
// handle success
dogPhoto.src = response.data.message;
})
.catch(function (error) {
// handle error
console.log(error);
});

步驟如下:

  1. 當我們執行上面的程式碼後, JavaScript 程序發出 ajax 請求,向 https://dog.ceo/api/breeds/image/random 拿取資料
  2. 同時,JS 也對瀏覽器說:「我接下來要暫停執行 axios 了,但你拿到資料後,請叫我,我會把 function(response) 叫回來執行 (I will call this function back)」
  3. 瀏覽器開始監聽,看是否有拿到資料
  4. 拿到資料之後,browser 就會把 callback function (a.k.a. function(response)) 排入 event loop 裡面的 callback queue 等待執行
  5. 把 callback queue 的 function(response) push 回去 JS engine 的 call stack 裡面正式執行。

如果還是不清楚怎麼運行,先確認自己已經了解 call stack/ Web APIs/ Callback Queue 的概念之後,輸入以下程式碼到 loupe 網站,loupe 會幫你視覺化實際操作情況:

console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');

以上的實例應該讓我們對 event loop 在做什麼有概念了。所以,event loop 的主要目的就是:

  1. 監控 call stack 和 callback queue。
  2. 如果 call stack 空了,那 event loop 就把 callback queue 的函式推進去 call stack 裡面,讓它執行。

最終,藉由 runtime environment 中 JavaScript engine、Web APIs、event loop 機制的集大成,終於可以讓 JavaScript 成為一個具備 single-threaded, non-blocking, asynchronous, concurrent 特性的語言。

--

--