[JS] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式(Learn event loop, stack, queue, and concurrency mode of JavaScript in depth)
keywords: event loop
, javascript
, queue
, stack
, concurrency model
, runtime
本篇內容整理自 Philip Roberts 在 JS Conf 的演講影片 What the heck is the event loop anyway? 和 MDN Concurrency model and Event Loop。
單線程(single threaded)
首先,我們要知道 JavaScript 是單線程(single threaded runtime)的程式語言,所有的程式碼片段都會在堆疊中(stack)被執行,而且一次只會執行一個程式碼片段(one thing at a time)。
堆疊(stack)
在 JavaScript 中的執行堆疊(called stack)會記錄目前執行到程式的哪個部分,如果進入了某一個函式(step into),便把這個函式添加到堆疊(stack)當中的最上方;如果在函式中執行了 return
,則會將此函式從堆疊(stack)的最上方中抽離(pop off)。
以下面的程式碼為例:
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
let squared = square(n);
console.log(squared);
}
printSquare(4);
當我們在執行 JavaScript 的函式時,首先進入 stack 的會是這個檔案中全域環境的程式(這裡稱作 main
);接著 printSquare
會被呼叫(invoke)因此進入堆疊(stack)的最上方;在 printSquare
中會呼叫 square()
因此 square()
會進入堆疊(stack)的最上方;同樣的,square
中呼叫了 multiply()
,因此 multiply
進入堆疊的最上方。
因此目前的執行的堆疊(call stack)會長的像這樣:
接著執行到每一個函式中的 return
或結尾時,這個函式便會跳離(pop off)堆疊。
Video: JavaScript Call Stack Demonstration
無窮迴圈
如果他是一個無窮迴圈,例如:
function foo() {
return foo();
}
foo();
那麼這個堆疊(Stack)將會不斷被疊加上去,直到瀏覽器出現錯誤:
Video: JavaScript Call Stack with Maximum Call Stack Size Exceeded
阻塞(blocking)
當執行程式碼片段需要等待很長一段時間,或好像「卡住」的這種現象,被稱作 阻塞(blocking)
,假設請求資料的 AJAX Request 變成同步(Synchronous)處理的話,那麼每 request 一次,因為必需等這個函式執行完畢從堆疊(stack)中跳離開後才能往下繼續走,進而導致阻塞的情形產生,以下面的 pseudo code 為例:
// pseudo code
var foo = $.getSync('//foo.com');
var bar = $.getSync('//bar.com');
var qux = $.getSync('//qux.com');
console.log(foo);
console.log(bar);
console.log(qux);
Video: JavaScript Call Back with Sync request
阻塞的情形導致瀏覽器停滯
Video: JavaScript Call Stack with Blocking
從上面的影片中可以看出當堆疊中有未處理完的函式導致阻塞產生時(以同步的方式模擬發出一個 request 但尚未回應前),我們沒辦法在瀏覽器執行其他任何動作,瀏覽器也無法重新轉譯(click 的 button 一直處於被按壓的狀態);必須要等到 request 執行結束後瀏覽器才會繼續運作。堆疊被阻塞(stack blocked)的情況會導致我們的瀏覽器無法繼續轉譯頁面,導致頁面「停滯」。
非同步處理與堆疊(Async Callback & Call Stack)
如果尚不清楚同步和非同步的差異,可參考 [筆記] 談談 JavaScript 中的 asynchronous 和 event queue
為了要解決阻塞的問題,我們可以透過非同步(Asynchronous)的方式搭配 callback ,在這裡我們以 setTimeout
來模擬非同步請求的進行,以下面的程式碼為例:
console.log('hi');
setTimeout(function () {
console.log('there');
}, 5000);
console.log('JSConfEU');
在執行這段程式的時候,執行堆疊(call stack)中會先執行 hi
, 接著執行 setTimeout
,但是在 setTimeout 中的這個回呼函式(callback function,簡稱 cb
)並不會立即被執行(等等會說明它到哪去了),最後堆疊中(stack)會在執行 JSConfEU
:
Video: JavaScript Call Stack with setTimeout