
英文 | https://javascript.plainenglish.io/6-interview-questions-that-combine-promise-and-settimeout-34c430fc297e
翻譯 | 楊小愛
在我們開始之前,我希望你能理清幾個知識點。
事件循環按以下順序執行:
- JS引擎中有兩個任務隊列:macrotask queue和microtask queue
- 整個腳本最初作為宏任務執行
- 執行時直接執行同步代碼,宏任務進入宏任務隊列,微任務進入微任務隊列
- 目前宏任務完成後,檢查微任務隊列,依次執行所有微任務
- 執行浏覽器 UI 線程的渲染(您可以在本文中忽略它)
- 如果存在任何 Web Worker 任務,則執行它(您可以在本文中忽略這一點)
- 檢查宏任務隊列,如果不為空,則傳回步驟2,執行下一個宏任務。
值得注意的是第4步:當一個macrotask完成後,先依次執行其他所有microtask,然後再執行下一個macrotask。
Mircotasks 包括:MutationObserver、Promise.then() 和 Promise.catch(),其他基于 Promise 的技術如 fetch API、V8 垃圾收集過程、node 環境中的 process.nextTick()。
Marcotasks 包括:初始腳本、setTimeout、setInterval、setImmediate、I/O、UI 渲染。
好吧,如果你不完全了解這裡發生了什麼,讓我們用例子來練習。
一共有 10 道題:前 4 道是簡單的 Promise 題,幫助你了解微任務;後面 6 個問題是 Promise 和 setTimeout 的混合。
1、
讓我們從一個簡單的例子開始來解釋微任務。
例子:
const promise1 = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
});
promise1.then(() => {
console.log(3);
});
console.log(4);
分析:
首先,執行此代碼的前四行,控制台會列印出1,然後promise1就會變成resolved狀态。
然後,開始執行 promise1.then(() => {console.log(3);}); 片段。因為 promise1 現在處于已解決狀态,是以 () => {console.log(3);} 将立即添加到微任務隊列中。
但是,我們知道 () => {console.log(3);} 是一個微任務,是以它不會立即被調用。
然後,執行最後一行代碼(console.log(4);),并在控制台列印 4。
至此,所有同步的代碼,即目前的宏任務,都被執行了。然後 JavaScript 引擎檢查微任務隊列并依次執行它們。
() => {console.log(3);} 然後執行并在控制台中列印 4。
結果如下:
2、
例子
const promise1 = new Promise((resolve, reject) => {
console.log(1);
});
promise1.then(() => {
console.log(3);
});
console.log(4);
分析:
這個例子和上一個非常相似,隻是在這個例子中,promise1 會一直處于挂起狀态,是以 () => {console.log(3);} 不會被執行,控制台也不會輸出3。
結果:
3、
例子
const promise1 = new Promise((resolve, reject) => {
console.log(1)
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('promise1:', promise1);
console.log('promise2:', promise2);
仔細考慮控制台列印結果的順序和每個 Promise 的狀态。
分析:
- 首先,前四行代碼和之前一樣,在控制台列印1,promise1的狀态是resolved。
- 然後,執行 const promise2 = promise1.then(...),res => {console.log(res)} 被添加到微任務隊列中。同時,promise1.then() 将傳回一個新的待處理的 promise 對象。
- 然後,執行console.log('promise1:', promise1); ,控制台列印出字元串'promise1'和處于已解決狀态的promise1。
- 然後,執行console.log('promise2:', promise2); ,控制台列印出字元串‘promise2’和處于挂起狀态的promise2。
- 至此,所有同步的代碼,即目前的宏任務,都被執行了。然後 JavaScript 引擎檢查微任務隊列并依次執行它們。
- res => {console.log(res)} 是微任務隊列中唯一的任務,現在将被執行。然後控制台将列印 'reslove1' 。
結果:
4、
例子
const fn = () => (new Promise((resolve, reject) => {
console.log(1)
resolve('success')
}));
fn().then(res => {
console.log(res)
});
console.log(2)
分析:
與之前不同的是,在這個例子中,建立 Promise 對象的行為發生在 fn 函數中。fn函數雖然是一個普通的同步函數,但并沒有什麼特别之處,這個例子還是很簡單的。
結果:
前面的例子比較簡單,現在問題會逐漸變得複雜,你準備好了嗎?
5、
例子:
console.log('start')
setTimeout(() => {
console.log('setTimeout')
})
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')
分析:
首先,JS引擎中有兩個任務隊列:宏任務隊列和微任務隊列。
在程式開始時,所有的初始代碼都被視為一個宏任務,被推入宏任務隊列。
然後,執行第一行代碼 console.log('start') 并在控制台中列印'start'。
那麼 ,setTimeout(...) 就是一個等待時間為 0 的定時器,會立即執行。正如我們在本文開頭提到的,setTimeout 是一個宏任務,是以 setTimeout(...), () => {console.log('setTimeout')} 的回調函數,不會立即執行,它會 被壓入宏任務隊列,等待稍後執行。
然後,它開始執行 Promise.resolve().then(...),并且 () => {console.log('resolve')} 被推入微任務隊列。
現在,執行console.log(‘end’),在控制台列印‘end’,第一個宏任務就完成了。
當一個宏任務完成後,JS引擎會先檢查微任務的隊列,然後,依次執行所有的微任務。
當微任務隊列為空時,JS引擎檢查宏任務隊列并開始執行下一個宏任務。
值得強調的是,雖然 setTimeout(...) 比 Promise.resolve().then(...) 執行得更早,但 setTimeout(...) 的回調函數仍然執行得較晚,因為 setTimeout 是一個宏任務。這是新手犯錯誤最多的地方。
好的,這就是上面示例代碼的運作方式。我希望我的草圖能幫到你。
結果:
6、
例子
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
分析:
首先,我們暫時忽略那些回調函數,簡化代碼:
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(..., 0);
console.log(2);
});
promise.then(...);
console.log(4);
然後我們像以前一樣繪制圖檔。起初,所有的代碼都可以被認為是一個宏任務。
然後開始執行new Promise(...),然後進入executor内部,執行console.log(1)。
然後開始執行 setTimeout(..., 0) 。定時器立即結束,其回調函數被推入宏任務隊列。
然後開始執行 console.log(2) 。
現在開始執行 promise.then(...)。因為promise對象還處于pending狀态,是以它的回調函數還沒有壓入微任務隊列。也就是說,微任務隊列目前仍然是空的。
然後開始執行 console.log(4) 。
至此,第一個宏任務結束,微任務隊列還是空的,是以JS引擎開始下一個宏任務。
然後,開始執行 console.log('timerStart') 。
現在 resolve() 函數被執行,promise 的狀态将被解析,promise.then(…) 的回調函數被推入微任務隊列。
然後,開始執行 console.log('timerEnd') 。
現在目前的宏任務已經結束,JS引擎再次檢查微任務隊列,依次執行。
結果:
7、
例子:
const timer1 = setTimeout(() => {
console.log('timer1');
const timer3 = setTimeout(() => {
console.log('timer3')
}, 0)
}, 0)
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
分析:
本例中有 3 個 setTimeout 函數,是以程式累加了 3 個額外的宏任務。
首先,讓我們繪制初始宏任務隊列。
然後,開始執行 timer1 對應的 setTimeout(...) 。同時,建立了一個新的宏任務。
然後,開始執行 timer2 對應的 setTimeout 。同時,另一個新的宏任務被建立。
好的,現在我們有了三個宏任務,沒有微任務。
然後
現在第一個宏任務和它的執行都完成了,而微任務隊列仍然是空的,JS引擎将開始執行下一個宏任務。
console.log('timer1') 被執行。
然後,開始執行 timer3 對應的 setTimeout(...) 。建立了一個新的宏任務。
然後
然後
結果
8、
例子
const timer1 = setTimeout(() => {
console.log('timer1');
const promise1 = Promise.resolve().then(() => {
console.log('promise1')
})
}, 0)
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
分析:
此示例與上一個示例類似,不同之處在于我們将其中一個 setTimeout 替換為 Promise.then。因為 setTimeout 是宏任務而 Promise.then 是微任務,并且微任務優先于宏任務,是以控制台輸出的順序是不一樣的。
首先,讓我們繪制初始任務隊列。
然後
然後
然後
注意此時 Promise.then() 正在建立一個微任務。它的回調函數在下一個宏任務之前由 JS 引擎執行。
然後
注意,此時Promise.then()正在建立一個微任務。它的回調函數在下一個宏任務之前由 JS 引擎執行。
然後結束。
結果:
9、
例子
const promise1 = Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
const promise2 = Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
分析:
在這個例子中,宏任務和微任務交替建立,這是一個困難的話題。如果你隻是在頭腦中思考,那麼,很容易犯錯誤。但是如果你開始和我一起畫圖,很容易找到正确的答案。
首先,讓我們繪制初始宏任務隊列。
然後執行第一段代碼,并建立一個微任務。
然後執行第二段代碼,并建立一個宏任務
然後
目前宏任務完成,微任務隊列中的任務開始。
然後,開始執行setTimeout(...)與 timer2 相關的并建立一個新的宏任務
目前的微任務隊列被清空,開始下一個宏任務。
然後,建立另一個微任務。
目前宏任務已完成,JS引擎再次檢查微任務隊列,發現隊列不為空,開始對微任務隊列中的任務進行優先級排序。
最後
結果
10、
例子
const promise1 = new Promise((resolve, reject) => {
const timer1 = setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
const timer2 = setTimeout(() => {
console.log('promise1', promise1);
console.log('promise2', promise2);
}, 2000)
分析:
- 首先,它通過new Promise(…) 建立了promise1,它處于pending 狀态。還建立了一個延遲為 1 秒的計時器。
- 然後,執行 const promise2 = promise1.then(...),因為 promise1 目前處于 Pending 狀态,是以 promise1.then() 的回調函數還不會加入到微任務隊列中。
- 然後,執行 console.log('promise1', promise1) 。此時,promise1 仍處于 Pending 狀态。
- 然後,執行 console.log('promise2', promise2) 。此時,promise2 仍處于 Pending 狀态。
- 然後,執行 const timer2 = setTimeout(…) 。還建立了一個延遲為 2 秒的計時器。
- 1000 毫秒後,timer1 完成。然後執行 thenresolve('success'),promise1 被解決。
- 調用 promise1.then(...) 的回調函數,并執行 throw new Error('error!!!')。抛出一個錯誤,promise2 被拒絕。
- 又過了 1000 毫秒,timer2 完成。() => {console.log('promise1', promise1); console.log('promise2', promise2);} 被執行。
結果:
總結
以上就是我今天與你分享的10道關于 Promise 和 setTimeout知識的面試題,希望這些面試題對你有幫助,如果你覺得有用的話,請記得點贊我,關注我,并将它分享給你身邊的朋友,也許能夠幫助到他。
最後,感謝你的閱讀,祝程式設計愉快!