我們越來越深入 JS 運作方式的重要部份了,今天要提到 「 Event Loop 」的概念,這是 JS 最獨特的地方,幾乎沒有其他語言有這個特性。
Outline
- Parts Of JavaScript Engine
- Event Queue
- Event Queue 運行流程
- Event Table
- Event Loop
Parts Of JavaScript Engine
之前提到過「 執行環境堆疊 」,函式呼叫時會產生執行環境,若在這個函式執行環境內還有其他函式被呼叫,就會在之上產生另一個執行環境,形成堆疊。而在上層的執行環境結束之前,下層部分的其他程式碼是無法被執行的 — 包含全域執行環境。
因此,只要在這之中某個堆疊執行過久,就算只有一個函式執行環境的堆疊,都有可能影響整個主程式( 全域執行環境 )的運行。不過應用程式裡面總是會有某些功能需要時間來提取 / 運算,這時候為了不讓整個主程式停下來等待太久,我們可以而且其實我們很常把這些比較耗時的工作放到主程式以外的另外一個部分去執行。
而在進入正題之前,必須先複習一下,前幾章節我們提到, JS 引擎底下有三個部分:
- 「 記憶體堆疊」
- 「全域執行環境」
- 「執行環境堆疊」。
然而瀏覽器內可不只有 JS 引擎,接下來我們要提到一個很重要的概念 — 「 Queue 」(又稱 Message / Event / Callback Queue )。
整個瀏覽器的運行環境並非只由 JS 引擎組成。因為 JS 語言特性屬於單執行緒,同時又為了讓網頁具有像「監聽事件」、「計時」、「 拉第三方API 」這些類似「背景作業」的功能,瀏覽器提供了另外一些部分來達成,分別是:
- Event Queue
- Web API
- Event Table
- Event Loop
整個由上述部分,包含 JS 引擎所組成的環境,也稱為 JS Runtime Environment ( JRE )
Event Queue
Queue (儲列)是什麼樣的概念呢? 我們先來看一下,在寫網頁程式的時候,有一些所謂「內建的」API 如 SetTimeout / setInterval ,這些 API 不存在於 JavaScript 原始碼內,但你仍然可以在開發時直接使用。因為這些 API 是屬於瀏覽器提供的 Web API 。Web API 並非 JS 引擎的一部分,但他屬於瀏覽器運行流程的一環。
關於 Web API ,舉一些例子:
- 操作 DOM 節點的 API 如 :document.getElementById
- AJAX 相關 API 像是:XMLHttpRequest
- 計時類型的 API ,就像剛剛提到的 setTimeout
這類 Web API 在與 JS 原始碼一起執行的時候,並不會直接影響 JS 主執行環境的運行,否則的話網頁在執行像是拉取第三方 API 資料的動作時,就只能乾等,無法執行任何其他事情了! 所以瀏覽器將這些必須等待執行結果的動作,丟給其他部分去執行,然後讓 JS 引擎可以繼續做他應該做的事情,上述提到要等待執行結果的行為,其實也就是「非同步」的行為。(因為不會一次直接從頭跑到尾做完)
這就是 Event Queue ( 事件儲列 )的工作了, 事件儲列專門用來存放這些非同步的函式,然後等到整個主執行環境運行結束以後,才開始依序執行事件儲列裡面的函式。而所謂 Queue 是一種「先進先出」的資料結構,與 Stack 的「後進先出」相反,所以先被推送到 Queue 裡面的函式會相對於其他函式優先被執行。
Event Queue 運行流程
下面會以 setTimeout 為例,解說 Event Queue的運行流程。
setTimeout(callbackFunction, timeToDelay)
像是 setTimeout 與 setInterval 這些計時的 API ,是在給定的時間到了之後,執行對應的函式內容。
function executeAfterDelay() {
console.log("I will be printed after 1000 milliseconds")
}
setTimeout(executeAfterDelay, 1000)
console.log("I will be executed first")
但在給定時間到達之後,確切來說也並非是直接執行,而是會等待整個 JS 的執行環境結束, Call Stack 清空了之後,才開始執行。像上面的程式碼,會在一秒後印出對應的 console 內容,但是 JS 引擎在看到 setTimeout 這個函式的時候,並不會停下來等一秒過後才繼續往下,而是會直接往下執行。
而在 JS 引擎繼續往下執行的時候,剛才我們呼叫setTimeout所造成的計時的動作依然在進行,直到一秒到了以後,瀏覽器會把給定的對應的函式推送到 Event Queue 裡面,然後等待主程式運行完畢。
整個流程看起來像這樣:
- JS 引擎執行到瀏覽器提供的 setTimeout 函式
- JS 引擎繼續運行,同時瀏覽器開始根據給定的秒數計時
- 等待計時完成後,把剛才給定的函式推送到 Event Queue 內
- 等待 JS 引擎運行完畢,主執行環境結束後,將 Event Queue 內的函式推送到 JS 主執行環境,產生堆疊(執行該函式)。
Event Table
Event Table 與 Event Queue 互相搭配的資料集合,他負責記錄在非同步目的達成後,有哪些函式或者事件要被執行,這裡指的非同步目的指的是像計時完畢、API資料獲取完畢、事件被觸發。當我們執行 setTimeout 這個函式時,JS 會把給定的函式與像是倒數的秒數之類的附帶資訊 ( meta data )推送到 Event Table裡面,等到一秒過後(目的達成)該函式就會被正式推送到Event Queue 等待執行。
Event Loop
那麼,什麼又是 Event Loop 呢?可以把 Event Loop 想成是另外一個幾乎無時無刻、每一毫秒都在執行的程式,他負責檢查現在主執行環境堆疊是否是空的?如果是空的,再去檢查 Event Queue ,若 Event Queue 有函式待執行,則將這些函式從 Event Queue 依序推出,並執行。
總結
在這個章節,其實你只要能夠了解 JS 內 Event Queue 的概念,知道setTimeout 內的函式是何時被執行、以及怎麼運作的,就可以抓住我想提的非同步運行方式的重點了,其他像是 Event Loop 、Event Table 都只是概念性的名詞解釋,如果你原本對 JS 的非同步特性不是很了解,希望上面的概念模型圖可以幫助到你。
這邊文章同時也會在 Medium 上的 Publication 分享,上面未來會有囊括 前端 / 後端 / DevOps / 資訊安全等相關的技術文章,如果有興趣歡迎追蹤。