【前端筆記】【面試題】setTimeout 和 promise 的執行順序為何
與 microtask & macrotask 又有什麼關係
今天有一個題目長這樣,試問印出的順序為何?
console.log("Start");
setTimeout(function() {
console.log("Timeout");
}, 10);
Promise.resolve ("promise1")
.then(function (str1) {
console.log (str1);
setTimeout(function() {
console.log("Timeout2");
}, 0);
})
.then(function () {
console.log ("promise2");
});
Promise.resolve("promise3")
.then(function (str2) {
console.log (str2);
})
.then(function () {
console. log ("promise4");
});
new Promise((resolve, reject) => {
console.log("new promise")
})
console.log("End");
答案會是
Start
new promise
End
promise1
promise3
promise2
promise4
Timeout2
Timeout
如果不太確定為什麼順序是這樣,表示你跟我面試的時候一樣,不知道什麼是 microtask 和 macrotask,以及執行順序是哪個優先。
解釋:
先從 Event Loop 聊起
Event Loop 是 JavaScript 運行環境的一個核心機制。
是為了解決 JavaScript 單線程(single thread)執行所帶來的潛在問題。在單線程環境中,長時間運行的任務(如複雜計算、call API 或計時器)可能會導致程序卡頓,影響用戶體驗。
因此這個機制允許將這些可能耗時或具有不確定完成時間的操作移出主執行線程(stack)。而 stack 可以繼續執行其他任務,當這些非同步的事情完成後,它們的 callback function 會被放入 queue。
每當 stack 空閒時,Event Loop 就會從 queue取出待執行的任務,將其放入 stack 執行。這個循環機制確保了 JavaScript 可以處理非同步操作,也同時維持其單線程的特性。
而 queue 其實又分為 microtask queue 和 macrotask queue,分別掌握不同的任務。如以下在 Web APIs 執行完非同步事件之後會依任務的不同丟到不同的 queue。
microtask (microtask queue):
包含以下的任務
promise
.then()
.catch()
.finally()
queueMicrotask()
MutationObserver
process.nextTick() (主要在 Node.js 中)
macrotask (macrotask queue):
包含以下的任務
setTimeout()
setInterval()
setImmediate() (主要在 Node.js 中)
requestAnimationFrame() (瀏覽器中)
I/O 操作
UI 渲染
主要特性為:
- microtask queue 和 task queue 都是 FIFO
- microtask 的優先度是比 macrotask 高的,當所有的 microtask 的 task 清空後才會去執行 macrotask,所以當有個無限迴圈將 task 放到 microtask,macrotask 就不會有執行到的那一天。
# 1. setTimeout vs. Promise
console.log("Start");
setTimeout(function() {
console.log("Timeout"); // macrotask!
}, 0);
Promise.resolve().then(function() {
console.log("Promise"); // microTask!
});
console.log("End");
因為 setTimeout 是屬於 macrotask,promise .then 是 microtask,所以結果是 Start End Promise Timeout
# 2. Promise Constructor vs. Promise
console.log("Start");
Promise.resolve().then(function() {
console.log("Promise"); // microTask!
});
new Promise((resolve, reject) => {
console.log("new promise")
})
console.log("End");
因為執行到 promise .then 會到放到 microtask queue,new Promise 在建立時就會直接在 stack 被執行,所以結果是 Start new promise End Promise
# 3. Promise then vs. Promise then
console.log("Start");
Promise.resolve ("promise1")
.then(function (str1) {
console.log (str1);
})
.then(function () {
console.log ("promise2");
});
Promise.resolve("promise3")
.then(function (str2) {
console.log (str2);
})
.then(function () {
console. log ("promise4");
});
console.log("End");
當執行完 Promise.resolve (“promise1”)
後以下 function 會被丟到 microtask queue :
function (str1) {
console.log (str1);
}
再來會是:
function (str2) {
console.log (str2);
}
接著會是第二 round 的 microtask queue :
function () {
console.log ("promise2");
}
最後是:
function () {
console.log ("promise4");
}
答案的順序是 Start End promise1 promise3 promise2 promise4
接著就可以回頭想想最一開始的題目為什麼是那樣的順序了!
References:
JavaScript Visualized — Event Loop, Web APIs, (Micro)task Queue
Difference between microtask and macrotask queue in the event loop
JavaScript Interview Preparation: Priority of callback, promise, setTimeout, and process.nextTick()