解答這個題目之前,先回顧下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');