你不知道的 async、await 魔鬼细节

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

👀做错了吧?

哈哈没关系

步骤拆分👇:

先执行同步代码,输出5
1、执行setTimeout,是放入宏任务异步队列中
2、接着执行async1函数,输出1
3、执行async2函数,输出3
4、Promise构造器中代码属于同步代码,输出4
async2函数的返回值是Promise,等待2个then后放行,所以AAA暂时无法输出
5、async1函数暂时结束,继续往下走,输出7
6、同步代码,输出11
7、执行第一个then,输出8
8、执行第二个then,输出9
9、终于等到了两个then执行完毕,执行async1函数里面剩下的,输出AAA
10、再执行最后一个微任务then,输出10
11、执行最后的宏任务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:
await 和 Promise.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、2,await拿到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 的时间
参考资料
[1]
https://juejin.cn/post/6844903715342647310#heading-3: https://juejin.cn/post/6844903715342647310#heading-3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容