1. 程式人生 > >“setTimeout、Promise、Async/Await 的區別”題目解析和擴充套件

“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讀取函式的時候,是先讀取的微任務,當微任務執行完畢之後,才會繼續執行巨集任務。流程可以參考下圖。

所以這個時候可以總結到事件迴圈中的執行順序

  • 同步 > 非同步
  • 微任務 > 巨集任務

那麼微任務和巨集任務都有什麼呢,簡單總結下就是:

  • 微任務:Promiseprocess.nextTick
  • 巨集任務:整體程式碼scriptsetTimeoutsetInterval

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');