Promise+async/await详解+实战演练

1,930 阅读15分钟

前言

最近做了一个项目, 是国内做前端开发, 绕不开的一个平台: 微信生态! 之前做过微信小程序, 这回这个则是微信公众号页面, 个人觉得微信小程序的开发体验比微信公众号页面的开发体验更好, 私以为是因为小程序的授权以及各种API更完善, 当然了, 也可能是因为我做的这个小程序比较简单吧, 关于微信公众号页面的具体内容可查看这篇文章: vue3+vant开发微信公众号网页爬坑不完全指北

这次的微信公众号开发前前后后有一个月的时间, 业务逻辑不算复杂, 但也不少, 主要是和微信进行各种交互, 各种流程, 这就少不了异步逻辑的处理, 对于前端开发来说, 异步逻辑的解决方案, 最著名和好用的莫过于async/await+promise了, 关于这个异步解决方案, 我之前也发过一篇相关的文章, 感兴趣的小伙伴可以看看: ES8 async/await: 优雅的异步编程解决方案, 而我个人还看过一篇非常著名的阮一峰老师的文章, 也就是大名鼎鼎的ECMAScript 6入门里的这篇文章: async 函数_ECMAScript 6 入门

本篇文章主要是从一些概念原理, 尤其是实际编写之后才发现的出发, 聊一聊我遇到的async/await+promise的实际使用场景, 那么话不多说, 我们正式开始

Promise

Promise.all等静态方法本文就不讨论了, 有需要的小伙伴可查阅阮一峰老师这本书的Promise相关章节: Promise 对象

首先还是我们所熟悉的Promise对象, 由Promise 构造函数生成:

const promise = new Promise((resolve, reject) => {
  //...

  if(/* 异步操作成功 */) {
    resolve(value);
  }else{
    reject(error);
  }
});

Promise 构造函数接收一个函数作为参数, 这个函数还有两个参数, 第一个参数我们叫resolve, 第二个我们叫reject, 分别是解决拒绝的意思

返回的Promise对象一共有3种状态: pending, fulfilled以及rejected

我们通过new操作符调用Promise的时候就是执行它的构造函数, 此时也会执行构造函数的参数, 以上面的代码为例就是会执行传递到Promise中的箭头函数, 此时返回的Promise对象的状态为pending, 当我们调用resolve或者reject的时候, Promise对象的状态将会改变: 调用resolve会使Promise对象的状态由pending变为fulfilled, 调用reject则会使其状态从pending变为rejected

同时需要注意的是, Promise的状态只能被改变一次, 要么从pengding变为fulfilled, 要么从pending变为rejected, fulfilledrejected之间不能互相变换更不能变回pending, 以及状态从pending变为rejected的时候将会抛出一个错误

resolve被调用时传递的参数将会变成Promise对象的结果, 该结果可以通过Promise对象的then方法获取到, 而reject被调用时传递的参数则会被Promise对象的catch方法获取到:

promise
  .then((res) => {
    //res就是调用resolve方法时传递的参数
    console.log(res);
  })
  .catch((error) => {
    //error是调用reject方法时传递的参数
    console.log(error);
  });

也就是说Promise对象的状态为fulfilled(调用了resolve方法之后)时, 我们能调用then方法获取resolve传递出来的参数, 而当它的状态是rejected(调用了reject方法之后)时, 我们则能调用catch方法获取reject传递出来的参数

这里能使用链式调用是因为Promisethen方法和catch方法都会返回一个Promise对象

async/await

await

await关键字必须要在async函数中使用, await的功能就像它的意思一样, 等待, 表示后面的表达式要等待一个结果, 而这个表达式一般是返回一个Promise对象, 也就是说await后面一般跟能返回Promise对象的表达式, 同时只有await等待的结果为fulfilled的时候才会执行它后面的语句, pendingrejected的时候都不会执行

简单来说就是await关键字会等待它后面的Promise fulfilled之后将Promise的结果做一个返回, 然后再接着执行后续的语句

由于await后面需要跟一个能返回Promise的表达式, 因此我们这里写一个函数, 它的返回值是一个Promise, 我这里用的是箭头函数, 它本质上是一个函数表达式, 写成函数声明的形式也是可以的, 但个人习惯, 因此还是写成箭头函数的形式, 这里也附上函数声明的写法:

async function foo() {

}

后续的写法均会使用箭头函数的形式:

const p = () => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        resolve(123);
      },
      2000
    )
  });
}

const foo = async () => {
  const res = await p();
  console.log(res);
  console.log('后续代码');
}

foo();

此时2秒之后才会打印res后续代码, 因为new的时候Promise的状态为pending, 2秒之后变为了fulfilled, 而如果是这样的情况:

const p = () => {
  return new Promise((resolve, reject) => {
    
  });
}

const foo = async () => {
  const res = await p();
  console.log(res);
  console.log('后续代码');
}

foo();

await后面的两个输出语句永远不会执行, 因为此时await后面的表达式返回的Promise对象的状态是pending的, 接下来再看看这段代码:

const p = () => {
  return new Promise((resolve, reject) => {
    reject('一个错误');
  });
}

const foo = async () => {
  const res = await p();
  console.log(res);
  console.log('后续代码');
}

foo();

此时await后面的两个输出语句也是永远不会执行, 因为Promise对象的状态是rejected的, 也就是说: async函数中, await语句后面的语句要执行, 当且仅当这个await后面的Promise的状态是fulfilled的时候才行

await最典型的用法就是继发的操作, 一个执行完成再执行下一个:

const p1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        resolve('p1');
      },
      2000
    )
  });
}
const p2 = () => {
  return new Promise((resolve, reject) => {
    resolve('p2');
  });
}

const foo = async () => {
  const res1 = await p1();
  const res2 = await p2();
  console.log(res1);
  console.log(res2);
}

foo();

这里2秒之后p1的状态变为fulfilled, 然后p2才会执行

这里可能有小伙伴要问了: rejected了怎么办? 这个问题我会放到接下来的内容中和大家探讨

async

现在我们知道await关键字必须要在async函数中才能使用, 相当于asyncawait提供了一个作用域, 但除此之外async还有什么别的作用吗? 私以为是有的, 因为我们的async函数只要执行了, 就会返回一个Promise对象, 而且这个Promise对象的状态默认是fulfilled的, 但如果async函数内显式返回了一个Promise对象, 那么最终async函数返回的Promise就是里面显式返回的Promise对象, 最终状态由内部显式返回的Promise对象决定, 简单来说就是:

  1. async内返回值不是Promise: 那个返回值会被Promise.resolve()处理之后返回, 此时async返回的Promise对象的状态为fulfilled
  2. async内返回了一个Promise: 直接返回这个Promise, 此时async返回的Promise对象的状态为内部Promise的状态

返回值不是Promise对象

const foo = async () => {
  
}

const res = foo();

console.log(res);

此时我们可以看到打印的res是个Promise对象, 同时它的状态为fulfilled, 只是结果是undefined, 因为async函数内没有任何的return语句来返回一个值:

const foo = async () => {
  
}

foo().then((res) => {
  console.log('res:', res);
});

这里可以看到打印的resundefined, 此时我们尝试另一种写法:

const foo = async () => {
  return 123;
}

foo().then((res) => {
  console.log('res:', res);
});

此时打印的res就有值了, 是123

返回值是Promise对象

我们直接来看代码:

const p = () => {
  return new Promise((resolve, reject) => {
    reject('p rejected');
  });
}

const foo = async () => {
  return p();
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

上面这段代码中, p函数返回了一个rejectedPromise, 然后async函数foo中直接返回这个Promise, 也就是说async的返回值已经是一个Promise对象了, 所以最终async返回的Promise就是p函数返回的Promise, 此时会进到catch回调, 因为p函数返回的是一个rejectedPromise

返回值前加不加await

先说结论哈: 个人认为是没必要加, 因为加不加结果都一样

async函数中返回Promise还有一种写法是在Promise前面加await关键字, 像这样:

const p = () => {
  return new Promise((resolve, reject) => {
    resolve('p resolved');
  });
}

const foo = async () => {
  return await p();
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

再对比看一下这段代码:

const p = () => {
  return new Promise((resolve, reject) => {
    resolve('p resolved');
  });
}

const foo = async () => {
  return p();
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

两段代码唯一的区别在于foo函数内的return语句, 前者有await关键字, 后者没有, 但最终结果是一样的, 都走到了foothen回调中, 为何会是一样的呢?

在讨论这个问题之前, 我们把这两段代码修改一下:

const p = () => {
  return new Promise((resolve, reject) => {
    resolve('p resolved');
  });
}

const foo = async () => {
  const res = await p();
  console.log('res:', res);
}

foo();
const p = () => {
  return new Promise((resolve, reject) => {
    resolve('p resolved');
  });
}

const foo = async () => {
  const res = p();
  console.log('res:', res);
}

foo();

关键代码在foo函数内, 一个是打印await p(), 一个是直接打印p(), 根据上面Promiseawait的知识能得出: await p()的返回值是字符串 p resolved, 而p()的返回值则是一个Promise, 再根据async的知识: async返回的结果是Promise则直接返回, 不是则会通过Promise.resolve处理之后返回可得出: 字符串 p resolved不是Promise对象, 它会被Promise.resolve处理成Promise, 处理成和p函数的返回值一样, 也就是说最终的结果都是p函数的返回值, 所以加不加await关键字, 结果都是一样的

处理多个异步操作

async函数有一个常用的做法是将多个await收敛起来做统一的处理, 也就是处理多个异步操作:

const p1 = () => {
  return new Promise((resolve, reject) => {
    resolve('p1');
  });
}
const p2 = (params) => {
  return new Promise((resolve, reject) => {
    resolve(`${params}_p2`);
  });
}

const foo = async () => {
  const res1 = await p1();
  const res2 = await p2(res1);
  return res2;
}

foo().then((res) => {
  console.log('res:', res);
});

这个使用方式也是async/await最常用的一个方式, 就是继发逻辑的处理, 而根据上面的知识, foo函数内也可以这么写:

const foo = async () => {
  const res1 = await p1();
  return p2(res1);
}

一个细节

async函数一经调用就会返回一个fulfilledPromise对象, 我们无法在其中根据条件来修改这个Promise对象的状态, 比如:

const foo = async () => {
  let res = 1;

  setTimeout(
    () => {
      res = 2;
    },
    3000
  );

  return res;
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

这段代码执行之后会走到foo().then方法中, 打印1, 哪怕使用了await关键字也不行:

const foo = async () => {
  let res = 1;

  await setTimeout(
    () => {
      res = 2;
    },
    3000
  );
  
  return res;
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

这段代码的运行结果和上面那段代码是一样的, 因此async函数的主要作用就是用来给await提供一个执行的作用域, 让我们能以同步的方式处理异步的逻辑

也就是说, 当我们需要处理一些逻辑, 在这些逻辑没有一个结果的时候Promise需要pending等待我们处理, 然后我们再根据处理的结果来决定这个Promise的状态究竟是fulfilled还是rejected, 此时只能使用一开始提到的Promise 构造函数来实现

错误处理

错误情况的处理是对await后面的Promise对象而言的, 但await又必须要在async方法中使用, 因此这里结合async来探讨, 以及这里主要聊一聊常用的几个情形

报错之后不中断后续代码执行

const p = () => {
  return new Promise((resolve, reject) => {
    reject('p rejected');
  })
}

const foo = async () => {
  const res = await p().catch((error) => {
    console.log('error:', error);
  });
  console.log('res:', res);
  console.log('后续代码');
}

foo();

这样的写法能捕获到报错, 同时还不会中断后续代码的执行

还有一个种写法是使用try...catch...语句:

const p = () => {
  return new Promise((resolve, reject) => {
    reject('p rejected');
  })
}

const foo = async () => {
  try {
    const res = await p();
    console.log('res:', res);
  } catch (error) {
    console.log('error:', error);
  }
  console.log('后续代码');
}

foo();

但一般try...catch...语句主要用于有多个await的情况

报错之后中断后续代码执行

这个是比较常见的一个情形, 比如这样的一段代码:

const p1 = () => {
  return new Promise((resolve, reject) => {
    reject('p1 rejected');
  })
}

const p2 = () => {
  return new Promise((resolve, reject) => {
    resolve('p2 resolved');
  })
}

const foo = async () => {
  try {
    const res1 = await p1();
    console.log('res1:', res1);
    const res2 = await p2();
    console.log('res2:', res2);
  } catch (error) {
    console.log('error:', error);
  }
}

foo();

由于await的特性, 当p1返回的Promise rejected之后, 后续的代码就不会执行了, 而对于多个await且上一个报错要中断下一个的执行, 那么用try...catch...语句来处理再好不过了

但上述的错误处理都是在async函数内进行的, 还有一种是需要将报错返回出去的情况, 当然了, 实际情况是需要将async内得到的结果返回出去, 无论是否报错, 未报错的情形上面已经探讨过了, 这里主要来看看报错的情形

将async中的成功/错误结果返回

先说返回错误的情形:

const p1 = () => {
  return new Promise((resolve, reject) => {
    reject('p1 rejected');
  })
}

const p2 = () => {
  return new Promise((resolve, reject) => {
    resolve('p2 resolved');
  })
}

const foo = async () => {
  const res1 = await p1();
  console.log('res1:', res1);
  const res2 = await p2();
  console.log('res2:', res2);
  console.log('error:', error);
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

这样就可以了, 就能将错误返回出去了, 任意一个await后面的Promise rejected了, 后续代码就会终止执行, 同时这种情况还等同于async返回的Promise对象被reject了, 可以理解为async会捕获它里面的错误, 确切的说是异常, 当然错误Error也算一种异常, 比如:

const foo = async () => {
  throw '出错了'; //抛出一个值为字符串 出错了 的异常
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });
const foo = async () => {
  throw new Error('出错了'); //抛出一个消息内容为 出错了 的Error对象
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

再来就是更常见的情况: 将最终结果返回, 无论这个结果是否报错, 此时我们可以这么做:

const p1 = () => {
  return new Promise((resolve, reject) => {
    resolve('p1');
  })
}

const p2 = (params) => {
  return new Promise((resolve, reject) => {
    resolve(`${params}_p2 resolved`);
  })
}

const foo = async () => {
  const res1 = await p1();
  return p2(res1);
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

这个情形中p1 p2fulfilled了, foo中的代码依次执行, 最终返回p2 fulfilledPromise, 再来就是报错:

const p1 = () => {
  return new Promise((resolve, reject) => {
    reject('p1 rejected');
  })
}

const p2 = (params) => {
  return new Promise((resolve, reject) => {
    resolve(`${params}_p2 resolved`);
  })
}

const foo = async () => {
  const res1 = await p1();
  return p2(res1);
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

此时由于p1报错, 则后续p2不会执行, 会直接报错, 同时被async捕获, 所以此时async函数返回的Promisep1 rejectedPromise

async中使用try...catch...并需要返回错误结果

这是我个人在实际开发工作中遇到的一个情况, 大致代码如下:

const p1 = () => {
  return new Promise((resolve, reject) => {
    reject('p1 rejected');
  })
}

const p2 = (params) => {
  return new Promise((resolve, reject) => {
    resolve(`${params}_p2 resolved`);
  })
}

const foo = async () => {
  try {
    const res1 = await p1();
    return p2(res1);
  } catch (error) {
    return error;
  }
}

foo()
  .then((res) => {
    console.log('res:', res);
  })
  .catch((error) => {
    console.log('error:', error);
  });

foo中执行了一个继发的异步操作, 成功则返回最终的结果, 也就是p2执行之后的结果, 任意一个报错了则返回这个错误, 但这个代码最终的执行结果却是进到了foo().then方法中, 而不是预期的foo().catch中, 这是为什么呢?

这是因为try...catch...中的catch捕获到的那个error已经不是一个Promise了, 而是一个异常, 在上面这个代码中, 这个异常 的类型是字符串, 而从上面async的知识中我们知道: 返回的结果如果不是Promise那么它会使用Promise.resolve处理一下然后再返回, 也就是说, 上面的foo函数中的代码等价于:

const foo = async () => {
  try {
    const res1 = await p1();
    return p2(res1);
  } catch (error) {
    return Promise.resolve(error);
  }
}

这么写也会进到foo().then方法中

项目中我在其他地方调用foo函数的时候遇到里面报错, 但最终foo函数返回的Promise居然是fulfilled的情况, 多方排查之后才发现了这个问题, 最终我将foo函数改为如下的形式之后, 运行结果就符合预期了:

const foo = async () => {
  try {
    const res1 = await p1();
    return p2(res1);
  } catch (error) {
    return Promise.reject(error);
  }
}

使用Promise.reject处理一下这个字符串, 那最终返回的就是rejectedPromise

但结合上面的知识我们不难发现, 这个foo函数还有一个写法也能符合我们的预期:

const foo = async () => {
  const res1 = await p1();
  return p2(res1);
}

p1报错, async能捕获这个错误并直接返回, p2报错也不用担心, 因为我们显式地写了return关键字

实际案例

一次请求多个接口渲染页面

这个私以为也是最常见的情形

比如这里有两个接口:

const api = {
  fetch1(delay) {
    return new Promise((resolve, reject) => {
      setTimeout(
        () => {
          resolve({
            data: 1,
            success: 1,
            msg: 'done'
          })
        },
        delay
      );
    })
  },
  fetch2(delay) {
    return new Promise((resolve, reject) => {
      setTimeout(
        () => {
          resolve({
            data: 2,
            success: 1,
            msg: 'done'
          })
        },
        delay
      );
    })
  }
};

每次请求都渲染页面

这里就会有些出入了, 首先我们来看看每次请求都渲染页面, 那么此时我们有两个设置数据的方法, 对应两个请求:

handleSetData1:

handleSetData1 = async () => {
  const res = await api.fetch1(2000);

  if(res.success) {
    //设置数据

    return true;
  }

  return Promise.reject(res.msg);
}

handleSetData2:

handleSetData2 = async () => {
  const res = await api.fetch2(100);

  if(res.success) {
    //设置数据

    return true;
  }

  return Promise.reject(res.msg);
}

为了模拟真实的请求, 这里分别给它们做了延迟处理. 然后还有一个统一的请求数据的方法:

getData:

getData = () => {
  Promise.all([
    this.handleSetData1(),
    this.handleSetData2()
  ]);
}

两个接口, 我们有各自的方法去请求并且做设置数据渲染页面的操作, 然后还有一个统一的getData的方法调用, 看似没有问题, 但却可能造成多次渲染的问题, 比如上述代码, 两个接口分别有1秒和100毫秒的延迟, 这会导致延迟更短的那个先执行, 然后渲染页面, 接着延迟长的后执行, 然后再次渲染页面, 造成多次渲染的问题, 但我们的目的是接口请求都回来之后统一渲染, 毕竟页面依赖两个接口的数据

理想情况下两个接口延迟非常之短, 那可能会只渲染一次, 但我们写代码, 不能寄希望于理想情况, 反而应该考虑一些边界问题, 比如这里的重复渲染问题, 既然需要请求完毕所有接口再渲染页面, 那么我们就改一改

全部请求完成再渲染页面

还是那两个api, 此时我们修改代码, 等它们都请求完毕之后再渲染页面:

getData:

getData = async () => {
  const res = await Promise.all([
    api.fetch1(2000),
    api.fetch2(100)
  ]);
  const [ res1, res2 ] = res;
  if(res1.success && res2.success) {
    this.setState({
      a: res1.data,
      b: res2.data
    })
  }
}

这样就能减少一次额外的渲染

每次请求都渲染页面2

不使用`

微信公众号权限验证配置

关于微信公众号开发相关的内容在这篇文章中有聊到, 需要的可以看一看: vue3+vant开发微信公众号网页爬坑不完全指北

handleWxConfig:

const handleWxConfig = (jsApiList) => {
  return new Promise((resolve, reject) => {
    api
      .retrieveWxJsSdkConfig() //这里可以换成实际的请求weixin-js-sdk配置的api
      .then((res) => {
        if (res.code === 0) {
          wx.config({
            // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
            debug: false,
            appId: res.data.appId, // 必填,公众号的唯一标识
            timestamp: res.data.timestamp, // 必填,生成签名的时间戳
            nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
            signature: res.data.signature, // 必填,签名
            jsApiList // 必填,需要使用的 JS 接口列表
          });
          wx.ready(function () {
            // config信息验证后会执行 ready 方法,所有接口调用都必须在 config 接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在 ready 函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在 ready 函数中。
            resolve(true);
          });
          wx.error(function (res) {
            // config信息验证失败会执行 error 函数,如签名过期导致验证失败,具体错误信息可以打开 config 的debug模式查看,也可以在返回的 res 参数中查看,对于 SPA 可以在这里更新签名。
            reject(res);
          });
        } else {
          reject(res.msg);
        }
      });
  });
};

微信公众号选择图片并获取本地图片数据

同样在vue3+vant开发微信公众号网页爬坑不完全指北中有提到

handleWxChooseImg:

const handleWxChooseImg = () => {
  return new Promise((resolve, reject) => {
    wx.chooseImage({
      count: 1, // 默认9
      sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
      sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
      success(res) {
        resolve(res);
        // 返回选定照片的本地 ID 列表,localId可以作为 img 标签的 src 属性显示图片
        // var localIds = res.localIds;
      },
      fail(error) {
        reject(error);
      }
    });
  });
};

handleWxGetLocalImgData:

const handleWxGetLocalImgData = (localId) => {
  return new Promise((resolve, reject) => {
    //获取本地图片
    wx.getLocalImgData({
      localId, // 图片的localID
      success(res) {
        // res.localData是图片的base64数据,可以用 img 标签显示
        //上传图片(axios, post直接扔base64, 貌似会转成form data类型上传, 而且还没有key)
        resolve(res);
      },
      error(error) {
        reject(error);
      }
    });
  });
};

handleGetImgBase64:

const handleGetImgBase64 = async () => {
  try {
    const res1 = await handleWxChooseImg();
    const res2 = await handleWxGetLocalImgData(res1.localIds[0]);
    return res2;
  } catch (error) {
    return Promise.reject(error);
  }
};

然后根据上面的知识, 不难发现我们还可以省略try...catch...语句写成这样:

const handleGetImgBase64 = async () => {
  const res1 = await handleWxChooseImg();
  const res2 = await handleWxGetLocalImgData(res1.localIds[0]);
  return res2;
};

但私以为还是保留try...catch...语句比较好, 不然可能会产生歧义: 这要是报错了怎么办, 而有了try...catch...就一目了然了, 毕竟try...catch...表示对可能出现异常的语句做一个容错的处理, 代码也是先给人读, 然后顺带让机器执行的, 人要是阅读起来有障碍, 那机器执行得再顺畅也无济于事

好的, 这就是这篇文章的全部内容了, 欢迎大家在评论区和我一起交流探讨, 最后, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需, 想看更多知识干货欢迎关注哦~

参考文献:

  1. Promise 对象
  2. async 函数
  3. async function