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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM