題目
請你說明以下程式碼會輸出什麼,以及盡可能詳細地解釋原因。
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
setTimeout(() => {
console.log(4);
}, 0);
console.log(5);
回答
JavaScript 背後的運作
這一題最主要的概念是 event loop 的運作機制,所以在解題之前必須先了解 JavaScript 的運作機制。
JavaScript 是一種 single thread 的程式語言,換言之,JS 的 Call Stack 只能照著程式碼的順序,一次做一件事情。但是在前幾周我們都學到了像是 eventListener
,Ajax
,setTimeout
這一類的技術,可以讓 JavaScript 在同一時間非同步的處理多項事件。
這是因為,如果不這麼做的話會造成很多效率低落的問題,所以 跑 JavaScript 的瀏覽器提供了一些 web API 給 JavaScript 使用,讓他可以做到這件事。
而為了了解這背後運作的原理,我們必須先補充 JavaScript 在 browser 以及 v8 engine 的運作原理,才有辦法理解這些運作。
在這邊推薦大家一些很棒的資源:
what the heck is the event loop anyway?
How JavaScript Works: An Overview of JavaScript Engine, Heap, and Call Stack
JavaScript 中的同步與非同步(上):先成為 callback 大師吧!
實際解題
把自己想像成執行 JavaScript 的瀏覽器,cosplay 它執行程式碼背後的機制,就可以推論出這些程式碼最後出現的行為。
在這邊指所以要將自己想像成「瀏覽器」而非「v8 engine」,是因為在實際執行的過程中,瀏覽器也會提供 web API 協助跑 JavaScript,而不是只有 v8 engine 單獨在跑。
Call Stack
前面有提到 JavaScript 是一個 single thread 的 程式語言,而這個 thread 所指稱的就是 Call Stack,Call Stack 會記錄下所有 function 執行所需要的 data 以及執行的進度(執行到哪一個 function 了)。
engine 會從全域開始建構 execution context,之後再依照各個 function 被呼叫的順序建構 function 的 execution context。
execution context 會在 Call Stack 裡依照順序堆疊 execution context,並且各該 function 被執行後,各該 execution context 也會離開 Call Stack,先進後出,後進先出。
web API
前面有提到 JavaScript 一次只能做一件事情,但像是 node.js,browser 這些執行環境會提供其他功能讓 JavaScript 可以完成非同步的工作。
在 browser 的環境裡,瀏覽器會提供像是 setTimeout
,DOM
,Ajax
這樣的 web API 讓 JavaScript 可以完成同步的工作。
但是我們都知到 JavaScript 只能在 Call Stack 執行,那瀏覽器到底是怎麼做到非同步的工作的呢?
瀏覽器會另外再開一個 thread 去跑這些非同步的 function 並將這些 function 裡面的 call back function 丟到 callback queue,等到 Call Stack 裡面跑完所有 function 淨空時,再依序把在 callback queue 的 function 丟到 Call Stack 執行。
event loop
現在我們知道那些 Callback function 會在 callback queue 等著,直到 Call Stack 空著的時候,才能進去 Call Stack 執行。
那這些 callback function 是怎麼知道 Call Stack 已經空了可以進去了呢?這就是 event loop 的職責了!
大家都去吃過一些很受歡迎的餐廳吧,通常在門口都會有一位站在門口負責查看是否有空位、引導客人的人員吧。event loop 的工作就像這位人員,不斷查看 call stack 是ㄇㄡ不斷查看 call stack 是否為空,如果有空,就把 callbakc queue 的那些 callback function 引導到 Call Stack 裡頭。
來開始跑看看吧!
有了前面基本知識的理解我們現在就來跑看看吧!
前面的程式碼太遠了,在這邊再放一組:
//Call Stack
console.log(1);
setTimeout(() =>{
conosle.lof(2);
}, 0);
console.log(3);
setTimeout(() =>{
conosle.log(4);
}, 0);
console.log(5);
在 Call Stack 裡
console.log(5);//->執行
setTimeout(() =>{
console.log(4);
}, 0) //-> 丟到瀏覽器所開的 thread 執行
conaole.log(3); //->執行
setTimeout(() =>{
conosle.log(2);
}, 0) //-> 瀏覽器另開一個 thread 丟去那裏執行
console.log(1); //-> 執行
在瀏覽器另外開的 thread 裡:
//thread(暫時稱他為 web API container)
setTimeout(() =>{
console.log(2);
}, 0)
setTimeout(() =>{
console.log(4);
}, 0)
零秒跑完之後,裡面的 callback function 跑到 callback queue:
//callback queue
() =>{
console.log(2)
}
() =>{
console.log(4)
}
event loop 檢查以下兩個條件,如果符合的話就將 callback queue 裡的 callback function 轉移到 Call Stack 裡面執行:
- Call Stack 已經清空。
- callback queue 有 function 在排隊中。
現在我們看看 Call Stack 裡面的狀況:
//Call Stack
console.log(5)//->執行完畢,離開 Call Stack
setTimeout(() =>{
console.log(4);
}, 0) //-> setTimeout 在另一個由瀏覽器所提供的 thread 執行;callback function 在 callback queue 排隊。
conaole.log(3); //->執行完畢,離開 Call Stack。
setTimeout(() =>{
conosle.log(2);
}, 0) //-> setTimeout 在另一個由瀏覽器所提供的 thread 執行;callback function 在 callback queue 排隊。
console.log(1) //-> 執行完畢,離開 Call Stack。
event loop 發先 Call Stack 為空,且 callback queue 裡面有 function 在排隊。所以將在 callback queue 裡等待的第一個 function 丟上去 Call Stack,等到這個 function 執行完畢後,在丟第二個上去 Call Stack。
第一個 function 拉近 Call Stack:
//Call Stack
() =>{
console.log(2);
} //從 callback queue 拉上來 Call Stack
//callback queue
() =>{
conosle.log(4);
}//待在 callback queue 裡等待 Call Stack 再次空掉。
再來,第一個 function 執行完畢,Call Stack 再次清空,event loop 引導第二個 function 上去:
//Call Stack
() =>{
console.log(4);
}
//callback queue
//淨空
結果
根據以上的運作機制,這三者會依序被執行:
console.log(1);
console.log(3);
console.log(5);
等到上面三個執行完後,才會依序執行以下兩者:
setTimeout(() =>{
console.log(2);
}, 0)
setTimeout(() =>{
console.log(4);
}, 0);
印刷出來的結果會長這樣:
1
3
5
2
4