8 張圖幫你一步步看清 async/await 和 promise 的執行順序

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')
複製代碼

注:由於是一道前端面試題,因此答案是以瀏覽器的eventloop機制爲準的,在node平臺上運行會有差別。前端

script start

 async1 start

 async2

 promise1

 script end

 promise2

 async1 end

 setTimeout
複製代碼

若是你發現運行結果跟本身想的同樣,能夠選擇跳過這篇文章啦,node

或者若是你有興趣看看俺倆的理解有沒有區別,能夠跳到後面的 「畫圖講解的部分」es6

須要具有的前置知識 promise的使用經驗面試

瀏覽器端的eventloopsegmentfault

不過若是是對 ES7 的 async 不太熟悉,是不要緊的哈,由於這篇文章會詳解 async。promise

那麼若是不具有這些知識呢,推薦幾篇我以爲講得比較清楚的文章瀏覽器

segmentfault.com/a/119000001… async異步

segmentfault.com/a/119000000… await最清楚的文章async

es6.ruanyifeng.com/#docs/promi… promise 的應該較少啦。函數

主要內容 第1部分:對於async await的理解 我推薦的那篇文章,對 async/await 講得更詳細。不過我但願本身能更加精煉的幫你理解它們。

這部分,主要會講解 3 點內容:

async 作一件什麼事情?

await 在等什麼?

await 等到以後,作了一件什麼事情?

補充: async/await 比 promise有哪些優點?(回頭補充)

1.async 作一件什麼事情?

一句話歸納: 帶 async 關鍵字的函數,它使得你的函數的返回值一定是 promise 對象。

也就是,若是async關鍵字函數返回的不是promise,會自動用 Promise.resolve() 包裝。

若是async關鍵字函數顯式地返回promise,那就以你返回的promise爲準。

這是一個簡單的例子,能夠看到 async 關鍵字函數和普通函數的返回值的區別:

async function fn1(){
   return 123
}



function fn2(){
   return 123
}



console.log(fn1())

console.log(fn2())

Promise {<resolved>: 123}



123
複製代碼

因此你看,async 函數也沒啥了不得的,之後看到帶有 async 關鍵字的函數也不用慌張,你就想它無非就是把return值包裝了一下,其餘就跟普通函數同樣。

關於async關鍵字還有那些要注意的?

在語義上要理解,async表示函數內部有異步操做

另外注意,通常 await 關鍵字要在 async 關鍵字函數的內部,await 寫在外面會報錯。

2.await 在等什麼?

一句話歸納: await等的是右側「表達式」的結果。

也就是說,右側若是是函數,那麼函數的return值就是「表達式的結果」。

右側若是是一個 'hello' 或者什麼值,那表達式的結果就是 'hello'。

async function async1() {

    console.log( 'async1 start')

    await async2()

    console.log( 'async1 end')

}

async function async2() {

    console.log( 'async2')

}

async1()

console.log( 'script start')
複製代碼

這裏注意一點,可能你們都知道await會讓出線程,阻塞後面的代碼,那麼上面例子中, async2 和 script start 誰先打印呢?

是從左向右執行,一旦碰到await直接跳出,阻塞 async2() 的執行?

仍是從右向左,先執行async2後,發現有await關鍵字,因而讓出線程,阻塞代碼呢?

實踐的結論是,從右向左的。先打印async2,後打印的 script start。

之因此提一嘴,是由於我常常看到這樣的說法,「一旦遇到await就馬上讓出線程,阻塞後面的代碼」。

這樣的說法,會讓我誤覺得,await後面那個函數, async2()也直接被阻塞呢。

3.await 等到以後,作了一件什麼事情?

那麼右側表達式的結果,就是await要等的東西。

等到以後,對於await來講,分2個狀況:

不是promise對象

是promise對象

若是不是 promise , await會阻塞後面的代碼,先執行async外面的同步代碼,同步代碼執行完,再回到async內部,把這個非promise的東西,做爲 await表達式的結果。

若是它等到的是一個 promise 對象,await 也會暫停async後面的代碼,先執行async外面的同步代碼,等着 Promise 對象 fulfilled,而後把 resolve 的參數做爲 await 表達式的運算結果。

第2部分:畫圖一步步看清宏任務、微任務的執行過程 咱們以開篇的經典面試題爲例,分析這個例子中的宏任務和微任務。

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')
複製代碼

先分享一個我我的理解的宏任務和微任務的慨念,在我腦海中宏任務和爲微任務如圖所示:

也就是「宏任務」、「微任務」都是隊列。

一段代碼執行時,會先執行宏任務中的同步代碼:

若是執行中遇到 setTimeout 之類宏任務,那麼就把這個 setTimeout 內部的函數推入「宏任務的隊列」中,下一輪宏任務執行時調用。

若是執行中遇到 promise.then() 之類的微任務,就會推入到「當前宏任務的微任務隊列」中,在本輪宏任務的同步代碼執行都完成後,依次執行全部的微任務一、二、3。

下面就以面試題爲例子,分析這段代碼的執行順序。

每次宏任務和微任務發生變化,我都會畫一個圖來表示他們的變化。

直接打印同步代碼 console.log('script start')

首先是2個函數聲明,雖然有async關鍵字,但不是調用咱們就不看。而後首先是打印同步代碼 console.log('script start')。

將setTimeout放入宏任務隊列

默認 所包裹的代碼,其實能夠理解爲是第一個宏任務,因此這裏是宏任務2:

調用async1,打印 同步代碼 console.log('async1 start')

咱們說過看到帶有async關鍵字的函數,不用懼怕,它的僅僅是把return值包裝成了promise,其餘並無什麼不一樣的地方。因此就很普通的打印 console.log('async1 start')。

分析一下 awaitasync2()

前文提過await,它先計算出右側的結果,而後看到await後,中斷async函數:

先獲得await右側表達式的結果。執行 async2(),打印同步代碼 console.log('async2'),而且return Promise.resolve(undefined)。

await後,中斷async函數,先執行async外的同步代碼。

目前就直接打印 console.log('async2'):

被阻塞後,要執行async以外的代碼。

執行 newPromise()

Promise構造函數是直接調用的同步代碼,因此 console.log('promise1'):

代碼運行到 promise.then()

代碼運行到promise.then(),發現這個是微任務,因此暫時不打印,只是推入當前宏任務的微任務隊列中。

注意:這裏只是把promise2推入微任務隊列,並無執行。微任務會在當前宏任務的同步代碼執行完畢,纔會依次執行:

打印同步代碼 console.log('script end')

沒什麼好說的。執行完這個同步代碼後,「async外的代碼」終於走了一遍

下面該回到 await 表達式那裏,執行 awaitPromise.resolve(undefined) 了。

回到async內部,執行 awaitPromise.resolve(undefined)

這部分可能不太好理解,我儘可能表達個人想法。

對於 awaitPromise.resolve(undefined) 如何理解呢?

developer.mozilla.org/zh-CN/docs/…

根據 MDN 原話咱們知道:若是一個 Promise 被傳遞給一個 await 操做符,await 將等待 Promise 正常處理完成並返回其處理結果。

在咱們這個例子中,就是 Promise.resolve(undefined) 正常處理完成,並返回其處理結果。那麼 awaitasync2() 就算是執行結束了。

目前這個promise的狀態是fulfilled,等其處理結果返回就能夠執行await下面的代碼了。

那什麼時候能拿處處理結果呢?

回憶平時咱們用promise,調用resolve後,什麼時候能拿處處理結果?是否是須要在then的第一個參數裏,才能拿到結果。

(調用resolve時,會把then的參數推入微任務隊列,等主線程空閒時,再調用它)。

因此這裏的 awaitPromise.resolve() 就相似於:

Promise.resolve( undefined)
.then((undefined) => {



})
複製代碼

把then的第一個回調參數 (undefined)=>{} 推入微任務隊列。

then執行完,纔是 awaitasync2() 執行結束。

awaitasync2() 執行結束,才能繼續執行後面的代碼,如圖:

此時當前宏任務1都執行完了,要處理微任務隊列裏的代碼。

微任務隊列,先進選出的原則:

執行微任務1,打印promise2

執行微任務2,沒什麼內容..

可是微任務2執行後, awaitasync2() 語句結束,後面的代碼再也不被阻塞,因此打印:

console.log( 'async1 end')

宏任務1執行完成後,執行宏任務2

宏任務2的執行比較簡單,就是打印:

console.log('setTimeout')

相關文章
相關標籤/搜索