JavaScript - Event-Loop

這次在練習一些前端常見的面試提時,遇到了一個有關 Event Loop 的題目問題,實在不是很暸解確切的執行過程,因此記錄一下學習筆記。

JavaScript - Event-Loop

關於 Event-Loop 在瀏覽器執行時的優點有兩項:

  1. Event Loop 協助了非同步請求的實現,並產生連貫的畫面呈現(influent UI)。 ![同步請求(Synchronous) v.s. 非同步請求(Asynchronous) (from: Alpha Camp)](miro.medium.com/max/1400/0*yO-MJbI-q0SmERE9.. "同步請求(Synchronous) v.s. 非同步請求(Asynchronous) (from: Alpha Camp")

  2. Event Loop 將「費時較久」或「須等待事件才能啟動」的任務往後安排,透過事件往後安排,能夠將網頁效能進行適當的配置,進而打造流暢以及較高的使用者體驗。

JavaScript 同步執行的缺點,搭配 Event Loop 進行解決

如果使用原生 JavaScript 那你應該經常會碰到資料渲染上的問題,當需要 render 的區塊,資料來源是使用第三方 API,在瀏覽器等待回應(Response)時,畫面會停在奇怪的顯示階段,預覽圖是有做 Loading 圖示,如果沒有做的話,使用者體驗更是糟糕。 ![瀏覽器等待 request 回應中](miro.medium.com/max/1400/0*zKoRxXjOdi0fZpzL.. "瀏覽器等待 request 回應中")

因此 Event Loop 就在這樣的情境下因應而生,可以說它是依附著瀏覽器而存在的一個事件監聽器,用以控管各項任務順暢執行。 當瀏覽器現階段沒有同步的 JS 代碼需要執行時,才會將排隊中的額外事件安排進去執行。

Event Loop 特性

這使用了下方三個形式,都會觸發 Event Loop 的效果:

  • Call stack 一個後進先出( LIFO = Last In, First Out)的執行堆疊(call stack)。會依序執行函式:從全域(Global Scope)的主程式(main program)開始,逐一把各個函式推(push)至執行堆疊的最上方,並從最後進入的函式開始執行。當函式結束後(reurn),會將此函式抽離(pop off)堆疊。

  • Web APIs 瀏覽器提供了很多不同的 API(例如:DOM、AJAX、Timeout),讓我們能夠同時(concurrently)處理多項任務。當完成 Web APIs 的內部函式後(如 setTimeout()),便將任務傳遞至工作佇列。

  • Callback Queue 這是一個先進先出( FIFO = First In, First Out)的工作佇列(callback queue) 。會接收從 Web APIs 來的任務,並透過 Event Loop 的監控,當堆疊中沒有執行項目時,便把佇列中的內容拉進堆疊中執行。

Event Loop (easy) 範例:

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  bar()
  baz()
}

foo()

/* foo
** bar
** baz
*/

透過以下流程圖,我們可以明白完整的執行順序: ![A simple event loop explanation (from: Flavio Copes)](miro.medium.com/max/1400/0*42kZatm9hw4u_BTu.. "A simple event loop explanation (from: Flavio Copes)")

Event Loop 進階版

接著我們花點時間思考一下,上面這支程式如何執行。應該可以得出以下結果:

console.log('Hi')

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

console.log('Bye')

/* Hi
** Bye
** cb1
*/

透過以下 GIF 動畫,可以幫助我們理解執行流程的全貌:

![Code execution in browser (from: SessionStack)](miro.medium.com/max/1400/0*0EpvJV7Wc6TLNtF6.. "Code execution in browser (from: SessionStack)")

Event Loop 搭配上 for Loop 迴圈 進階題

for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

/* 3
** 3
** 3
*/

這裡的答案是會 console 出三次的 3,明明 for 迴圈後方有限制了,無法讓 i 變數大於 3 ,但答案卻還是回傳了 3 ,為什麼呢?

執行詳細過程:

  1. for 迴圈結束時,變數的改變過程為 0,1,2 ,很合理的 i=2 變數沒有大於 3
  2. 透過前面的範例以及敘述,我們這裡會知道瀏覽器會將 setTimeout 放入了 queue 裡頭,最後才去執行
  3. 由於 var 的特性是屬於全域的狀況,會將 i=2 變數重新注入此函示去進行 console ,加上 for 迴圈特性,第一次進入的變數,不管 for 迴圈後方的限制,都會去執行第一遍,因此會在執行一次 i++ ,所以最後的答案會是 i=3

假如上述那題目改為 let 來進行宣告 i 變數呢?答案會是什麼呢?

for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}

/* 0
** 1
** 2
*/

:::tip

let 是區塊作用域,因此雖然 setTimeout 一樣會觸發 Event Loop,但變數不會重新回到全域當中,因此 console 出來的答案會是 console 出:0,1,2

:::