JavaScript - Event-Loop
這次在練習一些前端常見的面試提時,遇到了一個有關 Event Loop 的題目問題,實在不是很暸解確切的執行過程,因此記錄一下學習筆記。
JavaScript - Event-Loop
關於 Event-Loop 在瀏覽器執行時的優點有兩項:
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")
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 ,為什麼呢?
執行詳細過程:
- for 迴圈結束時,變數的改變過程為 0,1,2 ,很合理的
i=2
變數沒有大於 3 - 透過前面的範例以及敘述,我們這裡會知道瀏覽器會將
setTimeout
放入了 queue 裡頭,最後才去執行 - 由於 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
:::