「過程詳解」async await綜合題

wuwhs發表於2023-03-29

前言

如果你之前跟我一樣一直對async await熟悉又陌生的話(熟悉是可能每天都在用,陌生是針對一些組合題又丈二和尚摸不著頭腦),不妨可以邊看邊練,總結規律,相信會逐漸清晰並有所得。本文對每個案例都詳細描述了程式碼的執行流程,如有不妥歡迎指正。

async 函式return值

async函式預設會返回一個Promise物件,不管最後函式有沒有return值。但是針對具體的返回值情況,實際上表現會有所不同,下面分別看看。

return值為普通值

這裡的普通值是指基礎型別值(NumberStringBoolean等)和非thenable和非Promise的值

async function foo() {
  return 'foo'
}
foo().then(() => console.log('a'))
Promise.resolve()
  .then(() => console.log('b'))
  .then(() => console.log('c'))
// 輸出結果:a b c

很簡單,不出意外輸出a b c,也就是async函式在執行完成後是沒有等待的。

  1. foo()執行完成沒有等待,遇到thenconsole.log('a')放入微任務佇列;
  2. 繼續往下執行Promise.resolve(),遇到thenconsole.log('b')入隊,當前同步任務全部執行完成;
  3. 開始執行微任務佇列,首先取出並執行console.log('a')輸出a
  4. 然後取出並執行console.log('b')輸出b,此時遇到thenconsole.log('c')入隊;
  5. 最後取出並執行console.log('c')輸出c,至此微任務佇列清空,程式碼執行結束;

return值為thenable

所謂值為thenable是指定義了then方法的物件,可以是一個字面物件,也可以是一個Class例項。

class Bar {
    then(resolve) {
    resolve()
    console.log('then')
  }
}

async function foo() {
  // return new Bar()
  return {
    then(resolve, reject) {
      resolve()
      console.log('then')
    }
  }
}
foo().then(() => console.log('a'))
Promise.resolve()
  .then(() => console.log('b'))
  .then(() => console.log('c'))
// 輸出結果:then b a c

怎麼順序不一樣了呢?

如果async函式的返回值是一個thenable,等同於生成一個Promise,在foo函式執行完成,並且Promise狀態變更(resolve或者reject)後,還要等1個then的時長
  1. foo()返回thenable值,執行then方法,Promise狀態變更,執行console.log('then')輸出then,等待1個then時長;
  2. 繼續往下執行Promise.resolve(),遇到thenconsole.log('b')放入微任務佇列,當前同步任務執行完成;
  3. 開始執行微任務佇列,首先取出並執行console.log('b')輸出b,當前微任務佇列清空;
  4. 此時步驟1等待時長到期,遇到thenconsole.log('a')放入佇列,取出執行輸出a
  5. 繼續步驟3遇到thenconsole.log('c')放入佇列,取出執行輸出c,至此微任務佇列清空,程式碼執行結束;

這裡如果foo函式返回的thenable方法的狀態沒有變更,則後面的foo().then將永遠不會執行。

async function foo() {
  return {
    then(resolve, reject) {
      console.log('then')
    }
  }
}
foo().then(() => console.log('a'))
Promise.resolve()
  .then(() => console.log('b'))
  .then(() => console.log('c'))
// 輸出結果:then b c

return 值為Promise

return後面的值是Promise,比如 new Promise(resolve=>resolve())Promise.resolve

async function foo() {
  return Promise.resolve('foo')
}
foo().then(() => console.log('a'))
Promise.resolve()
  .then(() => console.log('b'))
  .then(() => console.log('c'))
  .then(() => console.log('d'))

// 輸出結果:b c a d

明顯可以看出async函式執行完後延遲了2個then時長。

  1. foo()返回Promise值,Promise狀態變更,等待2個then時長;
  2. 繼續往下執行Promise.resolve(),遇到thenconsole.log('b')放入微任務佇列,當前同步任務執行完成;
  3. 開始執行微任務佇列,首先取出並執行console.log('b')輸出b,當前微任務佇列清空;
  4. 遇到thenconsole.log('c')放入佇列,取出執行輸出c
  5. 此時步驟1等待時長到期,遇到thenconsole.log('a')放入佇列,取出執行輸出a
  6. 繼續步驟4遇到thenconsole.log('d')放入佇列,取出執行輸出d,至此微任務佇列清空,程式碼執行結束;

綜合上述表現可以總結出如下規律

await 表示式值

既然async函式返回值對程式碼執行順序有影響,那麼await後面的表示式值是否也有影響呢?下面同樣分為上述三種場景進行實驗分析

await值為普通值

async function foo() {
  await 'foo'
  console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
  .then(() => console.log('c'))
  .then(() => console.log('d'))
// 輸出結果:a c b d

可以判斷,await後面的表示式值如果是普通值,無須等待then時長。那麼,為什麼b會在c後面輸出呢?

await表示式有執行結果後,await下一行到函式結束部分程式碼codex可以看做放置到微任務佇列中,等同於Promise.resolve(await xxx).then(()=>codex),這裡是虛擬碼,await在時間順序上等效於Promise.prototype.then
  1. await 'foo'執行完成後,console.log('a')被新增到微任務佇列;
  2. 繼續往下執行同步任務Promise.resolve(),遇到thenconsole.log(c)新增到微任務佇列,當前同步任務執行完成;
  3. 然後執行微任務佇列中任務,取出並執行console.log('a')輸出a
  4. 此時foo函式執行完成,遇到thenconsole.log('b')入隊;
  5. 繼續執行微任務佇列中console.log('c')輸出c,此時遇到thenconsole.log('d')入隊;
  6. 最後依次執行取出剩餘微任務,執行並輸出bd,至此微任務佇列清空,程式碼執行結束;

await值為thenable

async function foo() {
  await {
    then(resolve) {
      resolve()
      console.log('then')
    }
  }
  console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
  .then(() => console.log('c'))
  .then(() => console.log('d'))
  .then(() => console.log('e'))
// 輸出結果 then c a d b e

await後面表示式值如果是thenable,需要等待1個then時長,才會去執行後續程式碼。

  1. foo()執行await是一個thenablePromise狀態變更,執行同步程式碼console.log('then'),輸出then,此時等待1個then時長;
  2. 繼續往下執行同步任務Promise.resolve(),遇到thenconsole.log('c')加入到微任務佇列,當前同步任務執行完成;
  3. 開始執行微任務佇列,取出並執行console.log('c'),輸出c,微任務佇列清空;
  4. 此時步驟1等待時長到期,將await後續程式碼console.log('a')入隊;
  5. 繼續步驟3,遇到thenconsole.log('d')入隊,然後依次取出console.log('a')console.log('d')並執行,輸出ad
  6. 執行完console.log('d')遇到thenconsole.log('e')放入佇列,取出執行,輸出e;

確實有點繞,我們將1個then等待時長看做是下一個微任務從入隊到執行完成出隊的時間就好。比如這裡c任務執行完成,下一個任務d正準備進入被a插了隊。

await值為Promise

async function foo() {
  await Promise.resolve('foo')
  console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
  .then(() => console.log('c'))
  .then(() => console.log('d'))
  .then(() => console.log('e'))
// 輸出結果 a c b d e

await後面表示式如果是Promise,和普通值的結果是一樣,無須等待then時長。
image.png
為什麼不和returnPromise的情景一樣是2次呢?原來這是nodejs在後期版本最佳化後的結果:移除了2個微任務,1個throwaway promise,具體原因可以檢視「譯」更快的 async 函式和 promises

image.png
對於早期版本(node 11及以前),輸出的結果是c d a e b,需要等待2個then等待時長。
image.png

  1. foo()執行await是一個PromisePromise狀態變更,此時等待2個then時長;
  2. 繼續往下執行同步任務Promise.resolve(),遇到thenconsole.log('c')加入到微任務佇列,當前同步任務執行完成;
  3. 開始執行微任務佇列,取出並執行console.log('c'),輸出c,微任務佇列清空;
  4. 遇到thenconsole.log('d')入隊,去除並執行,輸出d,微任務佇列清空;
  5. 此時步驟1等待時長到期,將await後續程式碼console.log('a')入隊;
  6. 繼續步驟4,遇到thenconsole.log('e')入隊,然後依次取出console.log('a')console.log('e')並執行,輸出ae
  7. 執行完console.log('a')遇到thenconsole.log('b')放入佇列,取出執行,輸出b;

綜合await表示式值的結果,我們可以總結

綜合async await

以上我們僅僅從asyncreturn值和await表示式值單一視角來看,下面綜合他們兩個來分析(統一在node 12+環境)。

await一個普通函式

首先,await是一個普通函式(非async函式)

function baz() {
  // console.log('baz')
  // return 'baz'

  // return {
  //   then(resolve) {
  //     console.log('baz')
  //     resolve()
  //   }
  // }

  return new Promise((resolve) => {
    console.log('baz')
    resolve()
  })
}

async function foo() {
  await baz()
  console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
  .then(() => console.log('c'))
  .then(() => console.log('d'))
  .then(() => console.log('e'))
// await baz函式return是普通值 輸出結果是baz a c b d e
// await baz函式return是thenable 輸出結果是 baz c a d b e
// await baz函式return是Promise 輸出結果 baz a c b d e

與直接await表示式值輸出一致。

  • baz函式return是普通值,不等待then時長;
  • baz函式returnthenable,等待1個then時長;
  • baz函式returnPromise不等待then時長;

await一個async函式

然後將baz函式改成async

async function baz() {
  // console.log('baz')
  // return 'baz'

  // return {
  //   then(resolve) {
  //     console.log('baz')
  //     resolve()
  //   }
  // }

  return new Promise((resolve) => {
    console.log('baz')
    resolve()
  })
}

async function foo() {
  await baz()
  console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
  .then(() => console.log('c'))
  .then(() => console.log('d'))
  .then(() => console.log('e'))
// await baz函式return是普通值 輸出結果是baz a c b d e
// await baz函式return是thenable 輸出結果是 baz c a d b e
// await baz函式return是Promise 輸出結果 baz c d a e b
// node12 以下版本 await baz函式return是Promise 輸出結果 baz c d e a b

從中我們可以發現:await async函式的等待時長與async baz函式的return值等待時長保持一致。

  • async baz函式return是普通值,不等待then時長;
  • async baz函式returnthenable,等待1個then時長;
  • async baz函式returnPromise等待2個then時長,但是在node12以下版本會等待3個then時長;

綜合async、await、Promise、then和setTimeout

下面我們綜合asyncawaitPromisethensetTimeout來看一道題目

const async1 = async () => {
  console.log('async1')
  setTimeout(() => {
    console.log('timer1')
  }, 2000)
  await new Promise((resolve) => {
    console.log('promise1')
    resolve()
  })
  console.log('async1 end')
  return Promise.resolve('async1 success')
}
console.log('script start')
async1().then((res) => console.log(res))
console.log('script end')
Promise.resolve(1)
  .then(Promise.resolve(2))
  .catch(3)
  .then((res) => console.log(res))
setTimeout(() => {
  console.log('timer2')
}, 1000)

思考幾分鐘,輸出結果

// script start
// async1
// promise1
// script end
// async1 end
// 1
// async1 success
// timer2
// timer1
  1. 執行同步任務輸出script startasync1,遇到setTimeout放入宏任務佇列;
  2. 繼續往下執行await表示式,執行new Promise輸出promise1Promise狀態變更,不等待then時長,將後續程式碼新增到微任務佇列;
  3. 繼續往下執行輸出script end,執行Promise.resolve(1)遇到thenPromise.resolve(2)放入微任務佇列;
  4. 再往下執行遇到setTimeout放入宏任務佇列,至此同步任務執行完畢;
  5. 開始執行微任務佇列,取出並執行步驟2的後續程式碼輸出async1 end,返回一個已變更的Promise物件,需要等待2個then時長;
  6. 繼續取出微任務Promise.resolve(2)並執行,狀態為resolved後面走then;
  7. 遇到then(res) => console.log(res)放入微任務佇列,然後取出並執行輸出1,注意:then中是非函式表示式會執行,預設返回的是上一個Promise的值then(Promise.resolve(2))會透傳上一層的1
  8. 此時步驟5等待時長到期,將(res) => console.log(res)放入微任務佇列,然後取出並執行輸出async1 success
  9. 最後2個定時器分別到期,輸出timer2timer1

如果對這個案例再稍作改造

const async1 = async () => {
  console.log('async1')
  setTimeout(() => {
    console.log('timer1')
  }, 2000)
  await new Promise((resolve) => {
    console.log('promise1')
  })
  console.log('async1 end')
  return 'async1 success'
}
console.log('script start')
async1().then((res) => console.log(res))
console.log('script end')
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .catch(4)
  .then((res) => console.log(res))
setTimeout(() => {
  console.log('timer2')
}, 1000)
// 輸出結果:
// script start
// async1
// promise1
// script end
// 1
// timer2
// timer1

具體過程就不一一列舉了,從輸出結果可以發現:如果await表示式的Promise的狀態沒有變更,以下程式碼以及後面的then永遠都不會執行then的執行時機是在前面函式執行完成並且Promise狀態變更以後才會被新增到微任務佇列中等待執行。

總結

透過以上就是基本async await的使用場景,以及綜合thenPromisesetTimeout的混合使用,大致可以總結如下幾條規律:

  • async函式的return值為thenable會等待1個then時長,值為Promise會等待2個時長;
  • await表示式值為thenable會等待1個then時長,值為Promisenode12+不等待then時長,低版本node等待2個then時長;
  • await一個async函式,async函式的return值為thenable會等待1個then時長,值為Promisenode12+會等待2個then時長,在低版本node等待3個then時長;
  • 如果then中是非函式,表示式本身會執行,預設返回的是上一個Promise的值,也就是透傳上一個Promise結果;
  • 如果await表示式的Promise的狀態沒有變更,以下程式碼以及後面的then永遠都不會執行;

以上案例均透過實驗執行得出,流程如有解釋錯誤,歡迎指正,完~

相關文章