不得不使用 Aysnc/Await 的原因

·614 Views·

目前 Async/Await 语法不论是在Node.js 端还是在浏览器端(依赖Babel)都支持的非常好。在和一些同学沟通中或者面试人中,也能听出他们对 Async/Await 有一定的了解,按理说他们写出来的代码应该都是或者大部分都用 Async/Await 替代 Promise 。但是,在后续的 Code Review 中,还是大量使用 Promise ,不停的 then then ... 充斥着嵌套地狱。Node.js 中,相对好点,大部分同学都在使用 Async/Await 这种方式 。

造成前后端使用上的区别,是受限于两者的开发思维。

对于后端开发者而言,代码执行的过程是同步的,写异步代码的情况比较少。如果使用 Promise.then 和 callback,反倒不容易理解和接受。比如数据库操作。早些年没有 Async 和 yield,直接用 callback ,代码是非常的晦涩,另外,在代码调试和定位方面,也是非常痛苦和棘手。

浏览器端情况就不同了,大家一开始接触的就是异步调用方式,现在需要变为「同步」的思维方式,反而不太自然了。

先从整体了解下它的特点。

Async/Await 简介

  • 它是一种全新的写「异步调用」的方式,之前是回调函数或Promise。
  • 它是基于 Promise 的同步语法糖。
  • 类似 Promise ,也是非阻塞。
  • 它的写法和行为看起来像同步代码,这也是它的魅力所在。

语法比较

假如某个页面展示之前,需要先请求数据,比如 fetchDetail ,它返回 Promise 对象,而这个 Promise 返回页面所需要的 JSON 数据。为了模拟调试过程,先简单把结果打印出来。

首先使用 Promise 的方式:

1// 省略部分Vue代码
2methods: {
3 fetchDetail() {
4 // 调用接口数据
5 return axios.get('<https://randomapi.com/api/b07f414474d47329b82840fba25dfabf>')
6 .then(response => response.data)
7 .then(response => response.results);
8 },
9},
10mounted() {
11 console.log('start mounted');
12 this.fetchDetail()
13 .then(products => {
14 this.products = products;
15 console.log('done', products);
16 });
17 console.log('end mounted');
18},
19// 结果会打印
20// start mounted
21// end mounted
22// done [...]


现在,我们改成 Async / Await 语法

1// 省略部分Vue代码
2async mounted() {
3 console.log('start mounted');
4 const products = await this.fetchDetail();
5 this.products = products;
6 console.log('done', products);
7 console.log('end mounted');
8},
9// 结果会打印
10// start mounted
11// done [...]
12// end mounted


可以看出以上两种方式的区别:

  • 在 「mounted」方法之前有个 「async」关键词,且 「await」关键词必须在有 「async」函数的内部出现。含有「async」的函数,会隐式返回一个 promise 对象。
  • 我们不能在顶层代码中直接使用 「await」调用,因为顶层代码没有「async」函数。这点在 Node.js 中会遇到。
1// app.js
2// 这里不会执行,因为 app.js 没有 async 关键词
3// const result = await fetchDetail();
4
5// 如果需要执行只能用 then
6fetchDetail().then(() => {
7 // TODO
8});
  • 「await」函数是“同步”的,从打印结果中明显可以看出。

为什么 Async/Await 更好?

代码清晰简洁明了
大家想象下,如果有多个请求,且下一个请求都是从上一个请求中获取数据,如果使用 Promise ,就会一直 then 嵌套 then ,陷入到嵌套地狱中。单从代码的可读性和可维护性方面就会大打折扣。而使用 Async ,则不会出现嵌套的情况,大大提高了代码的可读性和可维护性。

错误捕捉
针对 Promise 捕捉错误,需要谨慎处理。因为 Promise 是异步的,try catch 不能写在最外层简单。 拿上面的 fetchDetail 举例。

1try {
2 this.fetchDetail()
3 .then(products => {
4 this.products = products;
5 // 转换错误
6 const json = JSON.parse(undefined);
7 this.json = json;
8 console.log('done', products);
9 })
10 // 需要通过这种方式捕捉错误
11 // .catch(() => {
12 // console.info('error');
13 // });;
14} catch (error) {
15 console.error(error);
16}

在最外层直接 try catch ,是捕捉不到异常或错误,需要调用 Promise 中的 catch 才可以。
我们看看使用 async / await 如何捕捉错误。

1async mounted() {
2 console.log('start mounted');
3 try {
4 const products = JSON.parse(await this.fetchDetail());
5 this.products = products;
6 console.log('done', products);
7 } catch (err) {
8 // 会执行到这里
9 console.error(err);
10 }
11 console.log('end mounted');
12},

可以看到,这种捕捉方式,比较清晰简洁,同时也比较符合大家的开发思路。

条件处理
假设请求某条数据,收到响应后,需要根据响应值来决定下一步如何操作,就不得不在 then 中写条件判断,代码如下:

1mounted() {
2 this.fetchId()
3 .then((id) => {
4 // 这里是条件
5 if (id < 2) {
6 this.fetchDetail()
7 .then(products => {
8 this.products = products;
9 console.log('get products', products);
10 })
11 } else {
12 console.info('fetch other request');
13 }
14 });
15},


当然,实际情况可能比这个更为复杂。当你想添加或修改某个功能,估计想离职的心思都有了。
让我们看看使用 Async/Await ,如何处理这种情况。

1async mounted() {
2 const id = await this.fetchId();
3 if (id < 2) {
4 const products = await this.fetchDetail();
5 console.log('get products', products);
6 } else {
7 console.info('fetch other request');
8 }
9},

很明显,比上一个简洁清晰多了。

中间值处理
还以上个例子来说,只是这次某个接口,需要依赖前两个接口的数据,代码如下:

1mounted() {
2 // 请求 A 接口
3 this.fetchA(10)
4 .then(aId => {
5 // 请求 B 接口
6 this.fetchB(aId)
7 .then(bId => {
8 // 请求 C 接口
9 // 但 C 接口 依赖 aId 和 bId
10 this.fetchC(aId, bId)
11 .then(() => {
12 console.info('do something');
13 });
14 })
15 });
16},

和上一个情况大致类似,单看这些缩紧就够头痛了,更何况实际情况要比这个还要复杂。

1Async/Await 如何处理这种情况呢?
2async mounted() {
3 const aId = await this.fetchA(10);
4 const bId = await this.fetchB(aId);
5 const cId = await this.fetchC(aId, bId);
6},

await 任意代码
你可以用 await 同步语句,也可以是异步语句。举个例子

1async mounted() {
2 // 等同于 Promise.resolve(5)
3 const id = await 5;
4},

这看起来没有什么用,但是在我们写类库以及函数时,可以统一返回 Promise 对象,避免使用者再次转换为 Promise 对象。

总结

通过对比两者的区别,很容易看到 Async/Await 比 Promise 友好。

虽然 Async/Await 使异步代码不再明显,但它的直观性,大大提高代码的可读性和可维护性。

而我们不就是一直都在追求这个目标。

希望大家在自己代码中多多使用。不止为了自己,更重要的为了其他同学,使他们能够很轻松,友好的修改自己的代码。