Fork me on GitHub

async与await回味一下

 “如果你担心某种情况发生,那么它就更有可能发生。” —— 墨菲定律

  我们都知道asyncawait其实是promise的语法糖,在过去还没有generatorasync\await时,只使用promise处理异步问题很容易出现多层回调嵌套的情景,比如我们第二个异步操作依赖于第一个异步请求返回的数据,那我们就需要在resolve后从第一个then的对应回调方法中去传递这个值,同理要是之后还有对前面操作的依赖就会不断嵌套下去…

回调地狱

  举个例子:假设有一个场景,我们需要先根据用户的申请编号(applyNo)去影像系统拿对应的影像编号(imageNo)再根据这个影像编号去请求影像信息。使用promise来实现大概有如下的代码:

1
2
3
4
5
6
7
8
9
10
11
fetch('获取影像编号接口', {
// 请求头配置
}).then(res => {
const { imageNo } = res; //
fetch('获取影像信息接口', {
// 请求头配置
}).then(res => {
const { imageInfo } = res;
// ...潜在回调地狱
})
})

Async和Await带来了什么

  有了asyncawait后,我们是如何实现上述的逻辑呢?

1
2
3
4
5
async function fetchDocInfo(applyNo) {
let { imageNo } = await fetchImageNo(applyNo);
let { imageInfo } = await fetchImageInfo(imageNo);
return imageInfo;
}

  可以看到,通过这种方式实现,首先嵌套的问题没了,其次它更符合我们思考问题的流程,使我们能够以编写同步代码的方式去进行异步编程;这也是我个人看来async/await对开发体验而言带来的最大提升。

Async做了什么

  带async的函数最终会返回一个Promise对象,即使你在函数中返回的是一个普通变量,它也会通过Promise.resolve()封装后再返回。其次在我们coding的过程中,await需要写在async函数内部,否则会报错。

Await又做了什么

  await做了一件事,等!它会等它右侧表达式的结果。而且还要区分结果的类型!当表达式结果是Promise时,它会进入异步的等待流程,直到Promiseresolve,最后将resolve的值作为await的等待结果;如果表达式结果就是一个直接量,那这个结果就是await要等的值。

Async/Await的执行时机

  之前的博客有聊过EL的一些执行输出场景,现在就可以把缺少的async/await加进去一起讨论了,首先第一点是我个人对比了几个网络上常见的执行DEMO得出的结论:async函数内部在到达await表达式前,可以等价于Promise内的构造函数部分,即这块区域的代码是同步执行的。了解这点后,综合前文讨论的根据await等待的表达式结果类型判断即可正确得到我们的执行输出顺序。上个DEMO:

1
2
3
4
5
6
7
8
9
10
async function async1() {
console.log(1)
await async2()
console.log(2)
}
async function async2() {
console.log(3)
}
async1()
console.log(4)

  最终输出结果是1 3 4 2,为啥呢?首先调用栈先调用了async1(),然后内部先输出同步的1,然后调用async2async2返回一个promise同时它内部的输出一样是同步输出,再加外层的4,就是1 3 4的顺序,最后async2返回的promiseresolved了,await等待结束,输出2

  我自己在这里也改写了一个DEMO,来看看是否真得理解了:

1
2
3
4
5
6
7
8
9
10
11
async function async1() {
console.log(1)
console.log(await async2())
console.log(2)
}
function async2() {
console.log(3)
return Promise.resolve(4);
}
async1()
console.log(5)

  还是一步步看,调用栈执行async1(),然后同步输出1,然后async1内的第二个输出结果需要awaitasync2的返回值,然后执行async2,同步代码,输出3,由于async2最终返回的是一个resolved的Promise对象,还是一个异步的状态进入micro task队列维护,我们会继续执行我们的同步任务,即最外层的5,之后Promise.resolve传入的值返回被await等到,输出4,这里等待结束,继续输出之后的2,所以有最终结果1 3 5 4 2

  如果你看到这里了,可能已经大概摸得差不多了,那我再把上面这个DEMO的async2返回变为直接量会如何呢?

1
2
3
4
5
6
7
8
9
10
11
async function async1() {
console.log(1)
console.log(await async2())
console.log(2)
}
function async2() {
console.log(3)
return 4;
}
async1()
console.log(5)

  最终输出结果依旧是1 3 5 4 2

  综上,我们可以得到:await其实就是一个异步等待结果的过程,得到结果才会resolved从而执行后续代码,同时我们可以把整个async函数视作一个Promise的执行流程,在内部await前代码块等价于Promise构造中的同步代码块,在await后的代码可以理解为then方法中对应resolved的回调处理部分,当Promiseresolved后就会被回调。还是那句话,同步优先。

  真的盘清楚了?那你的Promise基础够硬么?如果我将Promise.resolve改成Promise.reject呢?

1
2
3
4
5
6
7
8
9
10
11
async function async1() {
console.log(1)
console.log(await async2())
console.log(2)
}
function async2() {
console.log(3)
return Promise.reject(4);
}
async1()
console.log(5)

  最后输出1 3 5…嗯?没了?

  可以看到控制台仅输出1 3 5,并且有一个未捕获的Promise值,这是因为我们并没有catch获取这个值,await也无法接收这个值,自然无法输出4,而之后的2resolved的回调而不是rejected的,自然也莫得~

  最后的最后,我们看一看某条的一道看烂的题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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");

  ①同步输出script start
  ②setTimeout进入宏任务队列;
  ③调用async1,同步输出async1 start
  ④await等待async2resolved的结果返回;
  ⑤执行async2,同步输出async2
  ⑥此时await还处于异步等待环节,await之后的等价于then中对应resolved的回调,进入微任务队列维护,然后继续处理优先级更高的同步问题;
  ⑦Promise的构造函数中同步输出promise1
  ⑧resolvethen回调放入微任务队列维护,此时微任务队列中有async1 endpromise2
  ⑨继续执行调用栈中的同步任务,输出script end
  ⑩此时同步任务已经全部跑完,我们回头看异步队列中维护的任务,由于微任务优先级高于宏任务,所以我们有async1 endpromise2setTimeout的输出顺序;

  那最后输出是否是上面说的这样呢?

  成了!