Event Loop

熟悉又陌生的東西

Hsu, Yang Min
9 min readMar 20, 2024

Event Loop 是在前端領域裡面常常見到的用語,到底 Event Loop 是什麼東西,又是為了解決什麼問題而出現的呢?
為了讓自己可以更瞭解背後運作的方式所以有了這篇文章。

JavaScript 是單執行緒的語言 (single thread)

JavaScript 是單執行緒的語言,代表他一次只能做一件事情,在這件事情完成之前沒有辦法去做別的事情。

這樣當 JavaScript 透過 Fetch 或是 XMLHttpRequest 來取得遠端資料或者用 setTimeout 來設定計時器的時候不就會停在那一行程式碼而不會往下執行嗎?

console.log('hello')
setTimeout(()=>{
console.log('你好')
},0)
console.log('world')

依照上面提到 JavaScript 單執行緒的特性應該會依序出現 hello你好world 才對。
但是實際執行程式碼我們看到的結果會是 helloworld你好,這是因為瀏覽器跟 node.js 這些 JavaScript 的執行環境都在背後做了一些事情,讓 JavaScript 在執行的時候不會被非同步的執行碼阻擋下來。

Call Stack

JavaScript 裡面是透過 Call Stack 來管理 function 的執行順序。

Call Stack 跟他的名字一樣,是一個 Stack 的資料結構來儲存跟紀錄當前執行的 function。

function fn1(){
console.log('fn1')
}
function fn2(){
console.log('fn2')
fn1()
}
function fn3(){
fn2()
console.log('fn3')
}
console.log('Hello')
fn3()

比如下面的程式碼在執行的時候 callStack 展開來看會是這樣。

  1. console.log("Hello") 被放到 call stack 裡面執行後馬上移出。
  2. fn3 執行,因此 fn3 被放到 call Stack 裡面。
  3. fn3 裡執行 fn2,因此 fn2 被放到 call Stack 裡面。
  4. fn2 裡執行 console.log("fn2")console.log("fn2") 放到 call stack 裡面執行後馬上移出。
  5. fn2 裡執行 fn1,因此 fn1 被放到 call Stack 裡面。
  6. fn1 裡執行 console.log("fn1")console.log("fn1") 放到 call stack 裡面執行後馬上移出,fn1 執行完成,fn1 移出 call stack。
  7. fn2 執行完成,fn2 移出 call stack。
  8. fn3 裡執行 console.log("fn3")console.log("fn3") 放到 call stack 裡面執行後馬上移出,fn3 執行完成,fn3 移出 call stack。

如果執行的 function 裡面還有執行其他 function,function 會一層層的向上堆疊,直到 function 內的所有 function 執行完畢才會被移出 call stack。

loupe 是一個觀察 event loop 的工具,但有一些新的語法會出現錯誤的結果。

從上面的結果可以知道在 JavaScript 裡面呼叫 function 都會被放到 call stack 裡面依序執行,如果這個時候有一個不知道會執行多久的 function 時不會就會把整個 stack 卡住嗎?

Web APIs

那當裡面加入了 setTimeout 之類的非同步事件時又是如何處理的呢?

function fn1(){
console.log('fn1')
}
function fn2(){
console.log('fn2')
fn1()
}
function fn3(){
fn2()
console.log('fn3')
}
console.log('Hello')

setTimeout(function timeout(){
console.log('timeout')
}, 1000)

fn3()

在這個例子中執行 fn3 之前設定了一個 1 秒的計時器,依照 JavaScript 單執行緒的特性,在 console.log("timeout") 執行之前應該不會執行 fn3 才對,然而把上面的內容貼到 console 可以看到跟預期的結果不同。

從 console 的結果看起來先執行了 fn3 才執行了 timeout,為什麼會這樣?JavaScript 不是單執行緒的程式語言嗎,在計時的同時不應該可以執行其他內容才對。

一樣用 loupe 來觀察看看。

會發現 setTimeout 執行後 timeout 被交給了「Web Apis」。

setTimeout 確實有進到 call stack 裡面,執行之後就交給了 web Apis 做計時的動作,當計時結束 Web Apis 會把要執行的內容交給放到 Callback Queue 裡面等待執行。

當 Call Stack 裡面沒有內容要執行的時候會在去 Callback Queue 裡面取「一個」待執行的 function 出來執行。

這個 Callback Queue 也可稱之為 Task Queue。

雖然被稱為 queue 但是根據文件其實是 sets

JavaScript 是單執行緒的語言但是因為有 Web Apis 的幫助,可以做到像是非同步的行為。

Task

那有哪一些 function 會交給 Web Apis 來幫忙處理呢?

除了範例裡面使用到的 setTimeout 以外,文件裡面提到 Dispatching Event、fetch、DOM 節點的操作等……,都會被視為 Task 而交給 Web Apis 來處理,等待時間結束後就會把任務放到 Task Queue 裡面等待執行。

console.log('Hello');
$.on('button', 'click', function onClick() {
console.log('You clicked the button!');
});

setTimeout(function timeout(){
console.log('timeout 1500');
}, 1500);

可以看到即使 Task Queue 裡面有兩個任務 Call Stack 還是只會取一個任務來執行,等到執行結束才會取下一個。

Micro Task

依照上面的理解,猜猜看下面 console.log 的順序為何?

setTimeout(function timeout(){
console.log('timeout');
},0);

Promise.resolve().then(()=>{
console.log('promise')
})

console.log('Hello')

結果會是 Hello > promise > timeout

這是因為 task 還可以再細分成普通的 task 跟 micro task。

當 Web Apis 等待結束會把 Task 依照 Task 的類型分別放到不同的 Task queue 裡面等待 call Stack 執行。

當 Call Stack 裡面的 function 都執行結束,內容為空時,會先去檢查 Micro task queue 裡面是否有任務待執行,然後會不斷檢查直到 micro task queue 為空時才會去取 Task queue 裡面的任務出來執行。

把上面的內容做成流程圖如下。

因為 Promise resolve 的結果被放到了 Micro task queue 裡面,當 call stack 被清空時發現 micro task queue 裡面有 Promise resolve 的結果,所以先取出來執行,然後確認 micro task queue 裡面已經沒有帶執行的任務時才會去取 task queue 的任務出來執行。

常見的 Task

  • Event Listener
  • DOM 元素的操作
  • 解析 HTML

常見的 Micro task

  • Promise
  • MutationObserver

反覆把任務交給 Web Apis 然後從 Task queue 裡面拿任務來 Call Stack 執行流程就是 Event Loop。

Event Loop

經過上面一系列了解之後可以知道幾件事情。

  • JavaScript 是單執行緒的程式語言,但是瀏覽器等其他執行環境可以讓 JavaScript 做到像是非同步的行為。
  • Event Loop 指的是當 JavaScript 遇到非同步的任務時,會把任務交給瀏覽器等執行環境做等待,等到任務可以執行時會在放到 Task queue 裡面準備執行,可以執行時再放到 JavaScript 的 Call Stack 執行。

Event Loop 確保 JavaScript 在單執行緒環境下正確處理非同步任務。
對於我們來說,理解 Event Loop 是非常重要的,因為它關乎著 JavaScript 的執行順序和性能優化。

--

--