你不知道的 async、await 魔鬼細節

作者:Squirrel_

https://juejin.cn/post/7194744938276323384

0、前言

關於promise、async/await的使用相信很多小夥伴都比較熟悉了,但是提到事件循環機制輸出結果類似的題目,你敢說都會?

試一試?

🌰1:

async function async1 () {
    await new Promise((resolve, reject) ={
        resolve()
    })
    console.log('A')
}

async1()

new Promise((resolve) ={
    console.log('B')
    resolve()
}).then(() ={
    console.log('C')
}).then(() ={
    console.log('D')
})

// 最終結果👉: B A C D

🌰2:

async function async1 () {
    await async2()
    console.log('A')
}

async function async2 () {
    return new Promise((resolve, reject) ={
        resolve()
    })
}

async1()

new Promise((resolve) ={
    console.log('B')
    resolve()
}).then(() ={
    console.log('C')
}).then(() ={
    console.log('D')
})

// 最終結果👉: B C D A

❓基本一樣的代碼爲什麼會出現差別,話不多說👇

1、async 函數返回值

在討論 await 之前,先聊一下 async 函數處理返回值的問題,它會像 Promise.prototype.then 一樣,會對返回值的類型進行辨識。

👉根據返回值的類型,引起 js引擎 對返回值處理方式的不同

📑結論:async函數在拋出返回值時,會根據返回值類型開啓不同數目的微任務

  • return 結果值:非thenable、非promise(不等待)

  • return 結果值:thenable(等待 1 個then的時間)

  • return 結果值:promise(等待 2 個then的時間)

🌰1:

async function testA () {
    return 1;
}

testA().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));

// (不等待)最終結果👉: 1 2 3

🌰2:

async function testB () {
    return {
        then (cb) {
            cb();
        }
    };
}

testB().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));

// (等待一個then)最終結果👉: 2 1 3

🌰3:

async function testC () {
    return new Promise((resolve, reject) ={
        resolve()
    })
}

testC().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));
    
// (等待兩個then)最終結果👉: 2 3 1




async function testC () {
    return new Promise((resolve, reject) ={
        resolve()
    })
} 

testC().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3))
    .then(() => console.log(4))

// (等待兩個then)最終結果👉: 2 3 1 4

看了這三個🌰是不是對上面的結論有了更深的認識?

稍安勿躁,來試試一個經典面試題👇

async function async1 () {
    console.log('1')
    await async2()
    console.log('AAA')
}

async function async2 () {
    console.log('3')
    return new Promise((resolve, reject) ={
        resolve()
        console.log('4')
    })
}

console.log('5')

setTimeout(() ={
    console.log('6')
}, 0);

async1()

new Promise((resolve) ={
    console.log('7')
    resolve()
}).then(() ={
    console.log('8')
}).then(() ={
    console.log('9')
}).then(() ={
    console.log('10')
})
console.log('11')

// 最終結果👉: 5 1 3 4 7 11 8 9 AAA 10 6

👀做錯了吧?

哈哈沒關係

步驟拆分👇:

  1. 先執行同步代碼,輸出5

  2. 執行setTimeout,是放入宏任務異步隊列中

  3. 接着執行async1函數,輸出1

  4. 執行async2函數,輸出3

  5. Promise構造器中代碼屬於同步代碼,輸出4

    async2函數的返回值是Promise,等待2then後放行,所以AAA暫時無法輸出

  6. async1函數暫時結束,繼續往下走,輸出7

  7. 同步代碼,輸出11

  8. 執行第一個then,輸出8

  9. 執行第二個then,輸出9

  10. 終於到了兩個then執行完畢,執行async1函數里面剩下的,輸出AAA

  11. 再執行最後一個微任務then,輸出10

  12. 執行最後的宏任務setTimeout,輸出6

❓是不是豁然開朗,歡迎點贊收藏!

2、await 右值類型區別

2.1、非 thenable

🌰1:

async function test () {
    console.log(1);
    await 1;
    console.log(2);
}

test();
console.log(3);
// 最終結果👉: 1 3 2

🌰2:

function func () {
    console.log(2);
}

async function test () {
    console.log(1);
    await func();
    console.log(3);
}

test();
console.log(4);

// 最終結果👉: 1 2 4 3

🌰3:

async function test () {
    console.log(1);
    await 123
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));

// 最終結果👉: 1 3 2 4 5 6 7

Note:

await後面接非 thenable 類型,會立即向微任務隊列添加一個微任務then但不需等待

2.2、thenable類型

async function test () {
    console.log(1);
    await {
        then (cb) {
            cb();
        },
    };
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));

// 最終結果👉: 1 3 4 2 5 6 7

Note:

await 後面接 thenable 類型,需要等待一個 then 的時間之後執行

2.3、Promise類型

async function test () {
    console.log(1);
    await new Promise((resolve, reject) ={
        resolve()
    })
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));

// 最終結果👉: 1 3 2 4 5 6 7

❓爲什麼表現的和非 thenable 值一樣呢?爲什麼不等待兩個 then 的時間呢?

Note:

  • TC 39(ECMAScript 標準制定者) 對await 後面是 promise 的情況如何處理進行了一次修改,移除了額外的兩個微任務,在早期版本,依然會等待兩個 then 的時間

  • 有大佬翻譯了官方解釋:更快的 async 函數和 promises[1],但在這次更新中並沒有修改 thenable 的情況


這樣做可以極大的優化 await 等待的速度👇

async function func () {
    console.log(1);
    await 1;
    console.log(2);
    await 2;
    console.log(3);
    await 3;
    console.log(4);
}

async function test () {
    console.log(5);
    await func();
    console.log(6);
}

test();
console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11

Note:

awaitPromise.prototype.then 雖然很多時候可以在時間順序上能等效,但是它們之間有本質的區別

  • test 函數中的 await 會等待 func 函數中所有的 await 取得 恢復函數執行 的命令並且整個函數執行完畢後才能獲得取得 恢復函數執行的命令;

  • 也就是說,func 函數的 await 此時不能在時間的順序上等效 then,而要等待到 test 函數完全執行完畢;

  • 比如這裏的數字6很晚才輸出,如果單純看成then的話,在下一個微任務隊列執行時6就應該作爲同步代碼輸出了纔對。


所以我們可以合併兩個函數的代碼👇

async function test () {
    console.log(5);

    console.log(1);
    await 1;
    console.log(2);
    await 2;
    console.log(3);
    await 3;
    console.log(4);
    await null;
    
    console.log(6);
}

test();
console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11

因爲將原本的函數融合,此時的 await 可以等效爲 Promise.prototype.then,又完全可以等效如下代碼👇

async function test () {
    console.log(5);
    console.log(1);
    Promise.resolve()
        .then(() => console.log(2))
        .then(() => console.log(3))
        .then(() => console.log(4))
        .then(() => console.log(6))
}

test();
console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11

以上三種寫法在時間的順序上完全等效,所以你 完全可以將 await 後面的代碼可以看做在 then 裏面執行的結果,又因爲 async 函數會返回 promise 實例,所以還可以等效成👇

async function test () {
    console.log(5);
    console.log(1);
}

test()
    .then(() => console.log(2))
    .then(() => console.log(3))
    .then(() => console.log(4))
    .then(() => console.log(6))

console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11

可以發現,test 函數全是走的同步代碼...

所以👉:**async/await 是用同步的方式,執行異步操作 **

3、🌰

🌰1:

async function async2 () {
    new Promise((resolve, reject) ={
        resolve()
    })
}

async function async3 () {
    return new Promise((resolve, reject) ={
        resolve()
    })
}

async function async1 () {
    // 方式一:最終結果:B A C D
    // await new Promise((resolve, reject) ={
    //     resolve()
    // })

    // 方式二:最終結果:B A C D
    // await async2()

    // 方式三:最終結果:B C D A
    await async3()

    console.log('A')
}

async1()

new Promise((resolve) ={
    console.log('B')
    resolve()
}).then(() ={
    console.log('C')
}).then(() ={
    console.log('D')
})

大致思路👇:

  • 首先,**async函數的整體返回值永遠都是Promise,無論值本身是什麼 **

  • 方式一:await的是Promise,無需等待

  • 方式二:await的是async函數,但是該函數的返回值本身是 ** 非thenable**,無需等待

  • 方式三:await的是async函數,且返回值本身是Promise,需等待兩個then時間

🌰2:

function func () {
    console.log(2);

    // 方式一:1 2 4  5 3 6 7
    // Promise.resolve()
    //     .then(() => console.log(5))
    //     .then(() => console.log(6))
    //     .then(() => console.log(7))

    // 方式二:1 2 4  5 6 7 3
    return Promise.resolve()
        .then(() => console.log(5))
        .then(() => console.log(6))
        .then(() => console.log(7))
}

async function test () {
    console.log(1);
    await func();
    console.log(3);
}

test();
console.log(4);

步驟拆分👇:

  • 方式一:

  • 同步代碼輸出1、2,接着將log(5)處的then1加入微任務隊列,await拿到確切的func函數返回值undefined,將後續代碼放入微任務隊列(then2,可以這樣理解)

  • 執行同步代碼輸出4,到此,所有同步代碼完畢

  • 執行第一個放入的微任務then1輸出5,產生log(6)的微任務then3

  • 執行第二個放入的微任務then2輸出3

  • 然後執行微任務then3,輸出6,產生log(7)的微任務then4

  • 執行then4,輸出7

  • 方式二:

  • 同步代碼輸出1、2await拿到func函數返回值,但是並未獲得具體的結果(由Promise本身機制決定),暫停執行當前async函數內的代碼(跳出、讓行)

  • 輸出4,到此,所有同步代碼完畢

  • await一直等到Promise.resolve().then...執行完成,再放行輸出3

方式二沒太明白❓

繼續👇

function func () {
    console.log(2);

    return Promise.resolve()
        .then(() => console.log(5))
        .then(() => console.log(6))
        .then(() => console.log(7))
}

async function test () {
    console.log(1);
    await func()
    console.log(3);
}

test();
console.log(4);

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})

// 最終結果👉: 1 2 4    B 5 C 6 D 7 3

還是沒懂?

繼續👇

async function test () {
    console.log(1);
    await Promise.resolve()
        .then(() => console.log(5))
        .then(() => console.log(6))
        .then(() => console.log(7))
    console.log(3);
}

test();
console.log(4);

new Promise((resolve) ={
    console.log('B')
    resolve()
}).then(() ={
    console.log('C')
}).then(() ={
    console.log('D')
})

// 最終結果👉: 1 4    B 5 C 6 D 7 3

Note:

綜上,await一定要等到右側的表達式有確切的值纔會放行,否則將一直等待(阻塞當前async函數內的後續代碼),不服看看這個👇

  • function func () {
      return new Promise((resolve) ={
          console.log('B')
          // resolve() 故意一直保持pending
      })
    }
        
    async function test () {
      console.log(1);
      await func()
      console.log(3);
    }
        
    test();
    console.log(4);
    // 最終結果👉: 1 B 4 (永遠不會打印3)
        
        
    // ---------------------或者寫爲👇-------------------
    async function test () {
      console.log(1);
      await new Promise((resolve) ={
          console.log('B')
          // resolve() 故意一直保持pending
      })
      console.log(3);
    }
        
    test();
    console.log(4);
    // 最終結果👉: 1 B 4 (永遠不會打印3)

🌰3:

async function func () {
    console.log(2);
    return {
        then (cb) {
            cb()
        }
    }
}

async function test () {
    console.log(1);
    await func();
    console.log(3);
}

test();
console.log(4);

new Promise((resolve) ={
    console.log('B')
    resolve()
}).then(() ={
    console.log('C')
}).then(() ={
    console.log('D')
})

// 最終結果👉: 1 2 4 B C 3 D

步驟拆分👇:

  • 同步代碼輸出1、2

  • await拿到func函數的具體返回值thenable,將當前async函數內的後續代碼放入微任務then1(但是需要等待一個then時間)

  • 同步代碼輸出4、B,產生log(C)的微任務then2

  • 由於then1滯後一個then時間,直接執行then2輸出C,產生log(D)的微任務then3

  • 執行原本滯後一個then時間的微任務then1,輸出3

  • 執行最後一個微任務then3輸出D

4、總結

async函數返回值

  • 📑結論:async函數在拋出返回值時,會根據返回值類型開啓不同數目的微任務

  • return 結果值:非thenable、非promise(不等待)

  • return 結果值:thenable(等待 1 個then的時間)

  • return 結果值:promise(等待 2 個then的時間)

await右值類型區別

  • 接非 thenable 類型,會立即向微任務隊列添加一個微任務then但不需等待

  • thenable 類型,需要等待一個 then 的時間之後執行

  • Promise類型 (有確定的返回值),會立即向微任務隊列添加一個微任務then但不需等待

  • TC 39 對await 後面是 promise 的情況如何處理進行了一次修改,移除了額外的兩個微任務,在早期版本,依然會等待兩個 then 的時間

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/cTBOa9B_7tpJyHDoLAn3JA