同步中的异步

在ES6中新增了​​asgnc...await...​​的异步解决方案,对于这种方案,有多种操作姿势,比如这样

const asyncReadFile = async function(){
const f1 = await readFile('/etc/fstab')
const f2 = await readFile('/etc/shells')
console.log(f1.toString())
console.log(f2.toString())
}

或者是这样

async function f(){
try{
await new Promise.reject('出错了')
} catch(e){

}
return await Promise.resolve('hello yerik')
}

是否能发现这两种使用方式的各自的特点:

  • ​async...await...​​异步解决方案支持同步的方式去执行异步操作
  • ​async...await...​​​异步解决方案支持通过​​try...catch...​​进行异常捕获

对于第一点来说还好理解,但第2种说法就很费解了,以至于有一种颠覆以往理解的绝望感,对于js的世界观都已经灰色。对于​​try...catch...​​​来说,不都是同步执行过程中捕获异常的吗,为何在​​async...await...​​​中的​​try...catch...​​可以捕获异步执行的异常呢?

这个时候就去翻一下阮一峰老师的ES6教程,还以为是我当年看书走眼了,忘了啥,查漏补缺,结果阮老师就这么轻飘飘一句话

跨越时空的对白——async&await分析_异步任务

┑( ̄Д  ̄)┍

时间和空间上的分离

阮老师,您说的是平行时空么?还是错位空间?

跨越时空的对白——async&await分析_回调函数_02

我吹过你吹过的晚风

那我们算不算 相拥

我遇到过你发现的error,那我们算不算相拥,反正我读完也是挺郁闷的,阮老师那种在大气层的理解,对于普通人的我还是需要一层层剖析才能理解,那就先按照自己的理解来说吧,大家一起探讨一下,看看有没有道理

我们知道对于​​nodejs​​​的异步实现都是借助​​libuv​​​其他线程完成的。正常情况下,当​​eventloop​​通知调用栈处理异步回调函数的时候,原调用栈种的函数应该已经执行完了,因此调用函数和异步逻辑是由完全不同的线程执行的,本质上是没有交集的,这个时候可以理解为空间上是隔离的。异步回调被触发执行时,调用函数早已执行结束,因而,回调函数和调用函数的执行在时间上也是隔离的

好了,时空隔离的问题,勉强解释通了,但是​​async...await...​​​又是怎么打破这种隔离,让其中的​​try...catch...​​​可以捕获到异步操作中的异常?曾经大胆猜测,​​async...await...​​​可以强行拉长​​try...catch...​​作用域,让调用函数的生命周期可以尽量延长,以至于可以等待直到异步函数执行完成,在此期间如果异步过程出现异常,调用函数就可以捕捉到,然而这个延长函数生命周期并等待异步执行结束,这不就是相当于是在阻塞线程的执行?阻塞执行——这跟JS的非阻塞的特质又是背道而驰的。

至此我总觉得在调用函数和异步逻辑之间存在某种诡异的tunnel,对!说的就是那股风!其可以在主函数和异步函数这两个不同时空互相隔离的生物进行消息传递,比如说在时空A中捕获了时空B里面的异常消息,这样它们就可以相拥❤

怎么想都觉得这个过程离大谱!

跨越时空的对白——async&await分析_异步分析_03

try...catch...不能捕获异步异常

​try...catch...​​​能捕获到的仅仅是​​try​​​模块内执行的同步方法的异常(try执行中且不需要异步等待),这时候如果有异常,就会将异常抛到​​catch​​中。

跨越时空的对白——async&await分析_协程_04

除此之外,​​​try...catch...​​​执行之前的异常,以及​​try...catch...​​内的异步方法所产生的异常(例如ajax请求、定时器),都是不会被捕获的!看代码

跨越时空的对白——async&await分析_异步任务_05

这段代码中,​​​setTimeout​​​的回调函数抛出一个错误,并不会在​​catch​​中捕获,会导致程序直接报错崩掉。

这说明在​​js​​​中​​try...catch...​​并不是说写上一个就可以高枕无忧。尤其是在异步处理的场景下。

那这个问题是怎么来的呢?

我从网上扒了个动图,可以比较形象的解释这个问题。图中演示了​​foo​​​,​​bar​​​,​​tmp​​​,​​baz​​​四个函数的执行过程。同步函数的执行在调用栈中转瞬即逝,异步处理需要借助​​libuv​​​。比如这个​​setTimeout​​​这个Web API,它独立于主线程中的​​libuv​​中别的线程负责执行。执行结束吼,会将对应回调函数放到等待队列中,当调用栈空闲吼会从等待队列中取出回调函数执行

跨越时空的对白——async&await分析_回调函数_06

const foo = ()=>console.log('Start!')
const bar = ()=>setTimeout(()=>console.log('Timeout!'), 0)
const tmp = ()=>Promise.resolve('Promise!').then(res=>console.log(res))
const baz = ()=>console.log('End!')

foo();
bar();
tmp();
baz();

不能捕获的原因

为了讲清楚不能被捕获的原因,我改一下代码,模拟异步过程发生了异常。大家可以把执行逻辑再套回刚才的动图逻辑再看一下,(后面有机会学习怎么做动图哈哈哈)

const bar = ()=> {
try{
setTimeout(()=>{
throw new Error()
}, 500)
}catch(e){
// catch error.. don't work
}
}

当​​setTimeout​​​的回调在​​Queue​​​排队等待执行的时候,​​Call Stack​​​中的​​bar​​​就已经执行完了,​​bar​​​的销毁顺便也终止了​​try...catch...​​​的捕获域。当主进程开始执行​​throw new Error()​​的时候,相当于外层是没有任何捕获机制的,该异常会直接抛出给V8进行处理


回调函数无法捕获?

因为大部分遇到无法​​catch​​​的情况,都发生在回调函数,就认为回调函数不能​​catch​​,这个结论是对的吗?

只能说不一定,且看这个例子

// 定义一个 fn,参数是函数。
const fn = (cb: () => void) => {
cb();
};

function main() {
try {
// 传入 callback,fn 执行会调用,并抛出错误。
fn(() => {
throw new Error('123');
})
} catch(e) {
console.log('error');
}
}
main();

结果当然是可以​​catch​​​的。因为​​callback​​​执行的时候,跟​​main​​​还在同一次事件循环中,即一个​​eventloop tick​​​。所以上下文没有变化,错误是可以​​catch​​的。 根本原因还是同步代码,并没有遇到异步任务。


如何捕获?

简单来说就是哪里抛异常就在哪里捕获

const bar = ()=> {
setTimeout(()=>{
try{
throw new Error()
}catch(e){
// catch error.. don't work
}
}, 500)
}

那这样写代码一点都不会快乐了,要出处小心,时候留意以防哪里没有考虑到异常的场景。

基于Promise的解决方案

所谓​​Promise​​​,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,​​Promise​​​ 是一个对象,从它可以获取异步操作的消息。​​Promise​​提供统一的 API,各种异步操作都可以用同样的方法进行处理。

本质上,这个就是一个状态管理机,同时又提供​​resolve​​​和​​reject​​​两个开关。​​resolve​​​负责将状态机的状态调整成​​Fulfilled​​​,​​reject​​​将状态处理成​​Rejected​​。

对于​​Promise​​来说是如何处理异常的?我们不妨通过改造前面的代码来试试


code1

function bar(){
new Promise((resolve, reject)=>{
setTimeout(()=>{
// 通过throw抛出异常
throw new Error('err')
}, 500)
})
}
function exec(){
try{
bar().then(res=>{
console.log('res', res)
})
}catch(err){
console.log('err has been caught in try-catch block')
}
}

在这个过程中,尝试抛出全局异常​​Uncaught Error​​​,然而​​try...catch...​​​并没有捕获到。造成这个问题的原因还是在于异常抛出的时候,​​exec​​​已经从执行栈中出栈了,此外,在​​Promise​​​规范里有说明,在异步执行的过程中,通过​​throw​​​抛出的异常是无法捕获的,异步异常必须通过​​reject​​捕获

跨越时空的对白——async&await分析_异步分析_07

code2

function bar(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
reject('err')
}, 500)
})
}
function exec(){
try{
bar().then(res=>{
console.log('res', res)
})
}catch(err){
console.log('err has been caught in try-catch block')
}
}

这次通过​​reject​​​抛出异常,但是​​try...catch...​​​同样还是没有捕获到异常。原因是​​reject​​​需要配合​​Promise.prototype.catch​​一起使用

跨越时空的对白——async&await分析_异步任务_08

code3

function bar(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
reject('err')
}, 500)
})
}
function exec(){
try{
bar().then(res=>{
console.log('res', res)
}).catch(err=>{
// Promise.prototype.catch捕获异常
console.log('err has been caught in promise catch')
})
}catch(err){
console.log('err has been caught in try-catch block')
}
}

这次,异常成功地通过​​Promise.prototype.catch​​​捕获到了,现在我们完全可以确定,在​​Promise​​​中,异常的捕获跟​​try...catch...​​没有什么关系。

跨越时空的对白——async&await分析_异步分析_09

code4

至此我们已然通过​​try...catch...​​​捕获异常的测试,那如果采用​​async...await...​​的方式呢?


function bar(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
reject('err')
}, 500)
})
}
async function exec(){
// trycatch 捕获异常
try{
await bar()
}catch(err){
console.log("err has been caught in try-catch block")
}
}

惊讶的发现,通过这样的方式,我们终于通过​​try...catch...​​​捕捉到了异常!对于code3和code4来说,我们的差异在于采用了​​async...await...​​,而这,到底是什么原理来实现的呢?至此,问题的根源我们已经模拟出来了,接下来是剖析

跨越时空的对白——async&await分析_协程_10

小结

​Promise​​​必须为以下三种状态之一:等待态​​Pending​​​、执行态​​Fulfilled​​​和拒绝态​​Rejected​​​。一旦​​Promise​​​被​​resolve​​​或​​reject​​​,不能再迁移至其他任何状态(即状态​​immutable​​。

跨越时空的对白——async&await分析_异步操作_11

基本过程:

  • 初始化​​Promise​​​状态​​pending​
  • 立即执行​​Promise​​​中传入的​​fn​​​函数,将​​Promise​​​内部​​resolve​​​、​​reject​​​函数作为参数传递给​​fn​​,按事件机制时机处理
  • 执行​​then(..)​​​注册回调处理数组(​​then​​​方法可被同一个​​promise​​调用多次)
  • ​Promise​​​里的关键是要保证,​​then​​​方法传入的参数​​onFulfilled​​​和​​onRejected​​​,必须在​​then​​方法被调用的那一轮事件循环之后的新执行栈中执行。

对于​​Promise​​​来说,本质上也是基于回调的,只要是基于回调,那就同样无法摆脱​​try...catch...​​​不能捕获异步异常的事实。不过在​​Promise​​​规范中有一套自己的异常处理逻辑,尽管这并不能打破时空上的隔离,但由于其将异步的异常逻辑封装在回调逻辑中,当​​Promise​​的状态发生改变时,将错误或异常以回调的形式呈现出来

虽然​​Promise​​​的出现很大程度改变了编程的习惯,不过嘛,这个机制还是有问题的,毕竟其运行的过程非常依赖内部状态的控制,我们知道​​Promise​​​的状态控制是非常依赖​​resolve​​​和​​reject​​​,这就意味着,我们必须很清楚明白异常会出现在哪里,然后异常出现的地方需要通过​​reject​​​的方式将​​Promise​​​的状态调整成​​Rejected​​​,也就说,我们需要很明确代码要在什么地方执行​​reject​

异常本无形,它的出现不一定可控,在工程实践的过程中又是大肠包小肠,层层套娃,​​Promise​​​可以处理我们已经明确的异常,那么那些不明确的又需要怎么处理呢?为了从本质上处理这个问题,​​async...await...​​由此而生


async&await今生

啰啰嗦嗦说了这么多,铺垫了​​async...await...​​​的诞生背景——为了解决异常跨越时空的问题,这部分则是解释​​async...await...​​​实现的原理,是的,就是那股风的来源,风起之处——​​Generator​


Generator

​Generator​​函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。


协程

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做协程​coroutine​​,意思是多个线程互相协作,完成异步任务。

对于协程来说其有点像函数,又有点像线程。它的运行流程大致如下

  • 协程A开始执行。
  • 协程A执行到一半,进入暂停,任务挂起,执行权转移到协程B。
  • (一段时间后)协程B交还执行权。
  • 协程A恢复执行。

上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。

举例来说,读取文件的协程写法如下。

function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}

上面代码的函数​​asyncJob​​​是一个协程,它的奥妙就在其中的​​yield​​​命令。它表示执行到此处,执行权将交给其他协程。也就是说,​​yield​​命令是异步两个阶段的分界线。

协程遇到​​yield​​​命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它如果去除​​yield​​命令,这种写法非常跟同步操作相比,不要说相似,简直一模一样。


协程的 Generator 函数实现

​Generator​​​函数是协程在​​ES6​​的实现,最大特点就是可以交出函数的执行权,即暂停执行。

整个​​Generator​​​函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用​​yield​​​语句注明。​​Generator​​函数的执行方法如下。


function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

上面代码中,调用​​Generator​​​函数,会返回一个内部指针(即遍历器)g。这是​​Generator​​​函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的​​next​​​方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的​​yield​​​语句,上例是执行到​​x + 2​​为止。

换言之,​​next​​​方法的作用是分阶段执行​​Generator​​​函数。每次调用​​next​​​方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是​​yield​​​语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示​​Generator​​函数是否执行完毕,即是否还有下一个阶段。


异常捕获

​Generator​​函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:

  • 函数体内外的数据交换
  • 错误处理机制。

注意观察代码中的两个​​next​​的不同

function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

​next​​​返回值的​​value​​​属性,是​​Generator​​函数向外输出数据

​next​​​方法还可以接受参数,向​​Generator​​函数体内输入数据。

上面代码中,第一个next方法的value属性,返回表达式​​x + 2​​​的值3。第二个​​next​​​方法带有参数2,这个参数可以传入​​Generator​​函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的value属性,返回的就是2(变量y的值)。

​Generator​​函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

上面代码的最后一行,​​Generator​​​ 函数体外,使用指针对象的​​throw​​​方法抛出的错误,可以被函数体内的​​try...catch​​代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。


异步任务的封装

下面看看如何使用​​Generator​​函数,执行一个真实的异步任务。

var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}

上面代码中,​​Generator​​​ 函数封装了一个异步操作,该操作先读取一个远程接口,然后从​​JSON​​​格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了​​yield​​命令。

执行这段代码的方法如下。

var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});

上面代码中,首先执行​​Generator​​​函数,获取遍历器对象,然后使用​​next​​​方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个​​Promise​​​对象,因此要用​​then​​​方法调用下一个​​next​​方法。

可以看到,虽然​​Generator​​函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。


co模块

​co 模块​​​是著名程序员​​TJ Holowaychuk​​​于 2013 年 6 月发布的一个小工具,用于​​Generator​​ 函数的自动执行。


var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

​co​​​模块可以让你不用编写​​Generator​​函数的执行器。

var co = require('co');
co(gen);

上面代码中,​​Generator​​ 函数只要传入co函数,就会自动执行。

​co​​​函数返回一个​​Promise​​​对象,因此可以用​​then​​方法添加回调函数。

co(gen).then(function (){
console.log('Generator 函数执行完成');
});

上面代码中,等到​​Generator​​函数执行结束,就会输出一行提示。


异步实现

先回答了异步实现的前置条件——基于协程,之后我们再来看看异步的关键词​​async​​。

​ES2017​​​标准引入了​​async​​函数,使得异步操作变得更加方便。

​async​​​函数是什么?一句话,它就是​​Generator​​函数的语法糖。

前文有一个​​Generator​​函数,依次读取两个文件。

const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

上面代码的函数​​gen​​​可以写成​​async​​函数,就是下面这样。


const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

一比较就会发现,​​async​​​函数就是将​​Generator​​​函数的星号​​(*)​​​替换成​​async​​​,将​​yield​​​替换成​​await​​,仅此而已。

​async​​​函数对​​Generator​​函数的改进,体现在以下四点。

  • 内置执行器。
asyncReadFile();
  • ​Generator​​​函数的执行必须靠执行器,所以才有了​​co​​​模块,而​​async​​​函数自带执行器。也就是说,​​async​​函数的执行,与普通函数一模一样,只要一行。

上面的代码调用了​​asyncReadFile​​​函数,然后它就会自动执行,输出最后结果。这完全不像​​Generator​​​函数,需要调用​​next​​​方法,或者用​​co​​模块,才能真正执行,得到最后结果。


  • 更好的语义。
  • ​async​​​和​​await​​​,比起星号和​​yield​​​,语义更清楚了。​​async​​​表示函数里有异步操作,​​await​​表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性。
  • ​co​​​模块约定,​​yield​​​命令后面只能是​​Thunk​​​函数或​​Promise​​​对象,而async函数的await命令后面,可以是​​Promise​​ 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  • 返回值是​​Promise​​。
  • ​async​​​函数的返回值是​​Promise​​​对象,这比​​Generator​​​函数的返回值是​​Iterator​​​对象方便多了。你可以用​​then​​方法指定下一步的操作。

进一步说,​​async​​​函数完全可以看作多个异步操作,包装成的一个​​Promise​​​对象,而​​await​​​命令就是内部​​then​​命令的语法糖。


async实现原理

本质上是将​​Generator​​函数和自动执行器,包装在一个函数里。

async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}

所有的​​async​​​函数都可以写成上面的第二种形式,其中的​​spawn​​函数就是自动执行器。

下面给出​​spawn​​函数的实现,基本就是前文自动执行器的翻版。

function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}

await分析

根据语法规格,​​await​​​命令只能出现在​​async​​函数内部,否则都会报错。

// 报错
const data = await fetch('https://api.github.com');

上面代码中,​​await​​​命令独立使用,没有放在​​async​​函数里面,就会报错。

目前,有一个​​语法提案​​​,允许在模块的顶层独立使用​​await​​​命令,使得上面那行代码不会报错了。这个提案的目的,是借用​​await​​解决模块异步加载的问题。

// awaiting.js
let output;
async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
}
main();
export { output };

上面代码中,​​awaiting.js​​​除了输出​​output​​​,还默认输出一个​​Promise​​​对象(async 函数立即执行后,返回一个​​Promise​​对象),从这个对象判断异步操作是否结束。

下面是加载这个模块的新的写法。

// usage.js
import promise, { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
promise.then(() => {
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
});

上面代码中,将​​awaiting.js​​​对象的输出,放在​​promise.then()​​​里面,这样就能保证异步操作完成以后,才去读取​​output​​。

这种写法比较麻烦,等于要求模块的使用者遵守一个额外的使用协议,按照特殊的方法使用这个模块。一旦你忘了要用​​Promise​​​加载,只使用正常的加载方法,依赖这个模块的代码就可能出错。而且,如果上面的​​usage.js​​​又有对外的输出,等于这个依赖链的所有模块都要使用​​Promise​​加载。

顶层的​​await​​命令,就是为了解决这个问题。它保证只有异步操作完成,模块才会输出值。

// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);

上面代码中,两个异步操作在输出的时候,都加上了​​await​​命令。只有等到异步操作完成,这个模块才会输出值。

加载这个模块的写法如下。

// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

上面代码的写法,与普通的模块加载完全一样。也就是说,模块的使用者完全不用关心,依赖模块的内部有没有异步操作,正常加载即可。

这时,模块的加载会等待依赖模块(上例是​​awaiting.js​​​)的异步操作完成,才执行后面的代码,有点像暂停在那里。所以,它总是会得到正确的​​output​​,不会因为加载时机的不同,而得到不一样的值。


小结

协程的引入具备了挂起自己和被重新唤醒的能力。可以想象一下,协程在被中断吼,是需要某种机制来保存当前执行的上下文。在空间上,协程初始化创建的时候为其分配的栈一定的栈空间,用来保存执行过程中的一些关键信息,当函数被唤醒后,通过栈内保存的信息恢复"案发现场"。

总结

至此,前面code4中的案例就解释通了,​​await​​​的时候​​exec​​​函数被挂起,等​​bar​​​函数中的异步操作执行结束后,​​exec​​​函数被恢复。此时恢复的还有​​try...catch...​​​。这个时候可以发现​​Promise​​​的状态已经通过​​reject​​​触发,由于没有​​Promise.prototype.catch​​​,所以这个时候​​Promise​​​会把异常向外抛出,正好被​​try...catch...​​​捕捉到,这个时候,确实如前文所猜测,在​​async...await...​​​中​​try...catch...​​就是守株待兔,并且最后还真的等到了!

跨越时空的对白——async&await分析_回调函数_12

Sync invoke an async function and await its returned awaitable object.


参考资料

  1. https://www.w3cschool.cn/escript6/escript6-q9rw37fc.html
  2. https://www.bookstack.cn/read/es6-3rd/spilt.3.docs-generator-async.md
  3. https://juejin.cn/post/6844903830409183239
  4. https://juejin.cn/post/6844904063570542599
  5. https://segmentfault.com/a/1190000009478377