await探究

这周在js群里讨论了一道今日头条的面试题,这题考察的是对microtask(微任务)和micratask(宏任务)的理解。题目和输出如下:

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
27
28
29
30
31
32
33
34
35
36
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');

// output:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

这里就不对微任务和宏任务进行深入探究了,这里我们来探讨一个有意思的小问题(假设你已经掌握了微任务和宏任务)。

很多人对于async1 end出现在script end之后,而不是出现在async2之后感到有所疑惑。

出现这个疑惑是因为对await的工作原理不够了解,下面我们来解释一下。

先说说为什么有的人会认为async1 end出现在script end之后。这是因为,在代码的书写上,由于使用了await关键字,所以可以使得我们用同步的代码去写异步的代码。

1
2
await async2();
console.log('async1 end');

很多人会觉得是js引擎在执行第一句的时候会暂停,等异步函数执行完了再去执行第二句,这就是误区所在。async/await的作用只是让我们像写同步代码一样写异步代码,而不是把异步代码变成同步代码。 这段代码还是异步的。

await只是promise的一个语法糖,我们对async1进行一个改写,你就能明白await的运行机制了。

1
2
3
4
5
6
7
8
9
10
11
12
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
// =>
function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
});
}

试着运行改写后的整段代码,是不是发现运行结果是一样的?

我们在来假设一下async2会返回参数,那么代码改写前后的效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
async function async1() {
console.log('async1 start');
let msg = await async2();
console.log(msg);
}
// =>
function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(msg => {
console.log(msg);
});
}

我想看到这里你就能明白了吧,这也就解释了为什么通过

1
let msg = await async2();

这样的写法,我们拿到promise拿到的结果。其实,我们并不是拿到了结果,而是把结果传给了msg这个形参。这放映了一个有意思的问题,代码的执行很可能和我们感觉的非常不一样。

我们在来看看上面的改写,不知道你有没有发现什么问题?

你应该一经发现了,对于promise而言,居然没有catch。很遗憾,这个就是使用await带来的代价。为了应对处理,我们需要使用try…catch来额外的捕获异常。目前还没有更好的解决方案,如果有请你告诉我,博客下面有我的邮箱。

对错误的处理,从另一个角度也说明了在异步嵌套较少情况下(一两层)使用async/await未必是最理想的选择。当然,这还的看业务完整逻辑来判断,比如我们请求一个借口,在封装好的接口内如果已经对错误进行了统一的预处理,那么之后使用await则不用再考虑这个问题。

最后在说一个小小问题,当我们把最开始的代码块放在浏览器里运行的时候,在promise2之后,setTimeout之前会输出一个undefined。这是因为我们通过console输入了一段代码段,而这段代码段会成为当前javascript主线程的执行脚本,在这段脚本执行完后(微任务也执行完才算),会有一个输出值给到控制台,这个输出是脚本最后一条语句/表达式的值。我们这里最后一句是:

1
console.log('script end');

而它执行值正是undefined

作者

Han Wei

发布于

2019-07-28

更新于

2024-04-07

许可协议

评论