“setTimeout、Promise、Async/Await 的區別”題目解析和擴充套件
解答這個題目之前,先回顧下JavaScript的事件迴圈(Event Loop)。
JavaScript的事件迴圈
事件迴圈(Event Loop)
:同步和非同步任務分別進入不同的執行"場所",同步的進入主執行緒,非同步的進入Event Table並註冊函式。當指定的事情完成時,Event Table會將這個函式移入Event Queue。主執行緒內的任務執行完畢為空,會去Event Queue讀取對應的函式,進入主執行緒執行。上述過程會不斷重複,也就是常說的Event Loop(事件迴圈)。流程可以參考下圖。
上面的話裡我們需要注意到Event Queue
這裡是分兩種情況的,即巨集任務(macrotask)
微任務(microtask)
,當主執行緒任務完成為空去Event Quenu
讀取函式的時候,是先讀取的微任務,當微任務執行完畢之後,才會繼續執行巨集任務。流程可以參考下圖。所以這個時候可以總結到事件迴圈中的執行順序
- 同步 > 非同步
- 微任務 > 巨集任務
那麼微任務和巨集任務都有什麼呢,簡單總結下就是:
- 微任務:
Promise
,process.nextTick
。 - 巨集任務:
整體程式碼script
,setTimeout
,setInterval
setTimeout、Promise、Async/Await詳解
setTimeout
定時器,可以延遲執行,屬於巨集任務,在JavaScript事件迴圈中,執行優先順序最低,可以執行下面的程式碼得到結果
console.log('script start')
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
//執行結果: script start -> script end -> settimeout
解析一下上面的程式碼:
- 同步執行,遇到setTimeout,將其放入非同步佇列中,跳過繼續執行,輸出script start -> script end
- 當同步任務佇列執行完畢,拿到非同步佇列中的setTimeout,輸出settimeout
上面的題可以直接聯想到另外一道經典的面試題就是
setTimeout(fn,0)的作用和原因?
Promise
Promise本身是同步的立即執行函式, 當在executor中執行resolve或者reject的時候, 此時是非同步操作, 會先執行then/catch等,當主棧完成後,才會去呼叫resolve/reject中存放的方法執行,列印p的時候,是列印的返回結果,一個Promise例項。resolve函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;reject函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。這個時候可以再執行一段程式碼檢視結果
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
//輸出結果:script start->promise1->promise1 end->script end->promise2->settimeout
解析一下上面的程式碼
- 同步執行script start
- 因為Promise本身是同步的立即執行函式,所以輸出promise1,
resolve()
的作用是改變Promise物件的狀態,並不會阻斷函式的執行,所以會執行輸出promise1 end。then
方法因為是非同步回撥微任務,所以會放入到微任務佇列中。跳出執行 - 遇到setTimeout,放入巨集任務佇列,跳過執行。
- 輸出script end,同步任務佇列執行完畢,然後去微任務佇列檢視有無執行函式,獲得promise1函式的then方法,輸出promise2,此時微任務佇列為空,然後去巨集任務佇列檢視有無執行方法,輸出settimeout。
async/await
async 函式返回一個 Promise 物件,當函式執行的時候,一旦遇到 await 就會先返回,等到觸發的非同步操作完成,再執行函式體內後面的語句。可以理解為,是讓出了執行緒,跳出了 async 函式體。可以執行下面的程式碼檢視結果
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
//輸出結果:script start->async1 start->async2->script end->async1 end
解析一下上面的程式碼:
- 同步執行,輸出script start
- 執行async1()函式,輸出async1 start,這是遇到await語句,執行await方法,但是後面的語句放入微任務佇列。
- 執行async2()函式,輸出async2
- 繼續執行同步佇列,輸出script end。此時同步佇列執行完畢,微任務佇列檢視有無執行函式或方法,輸出async1 end
- 此時微任務佇列為空,然後去巨集任務佇列檢視有無執行方法。
總結
settimeout的回撥函式放到巨集任務佇列裡,等到執行棧清空以後執行; promise.then裡的回撥函式會放到相應巨集任務的微任務佇列裡,等巨集任務裡面的同步程式碼執行完再執行;async函式表示函式裡面可能會有非同步方法,await後面跟一個表示式,async方法執行時,遇到await會立即執行表示式,然後把表示式後面的程式碼放到微任務佇列裡,讓出執行棧讓同步程式碼先執行。
最後,給大家提供個究極問題,自己思考下答案然後列印對比下吧
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');