一篇文章搞定 async/await ,再用起来得心应手

121 阅读7分钟

一篇文章搞定 async/await ,再用起来得心应手

async /await 是什么?

async/await 是 ECMAScript 2017 引入的 异步编程解决方案

Promise 也是 异步编程解决方案,那为什么又要引入 async/await呢?

ECMA-262 中 是这么说的:

  • 用于改进异步编程体验。它们提供了更简洁的语法来定义返回 Promise 的函数
  • 允许开发者编写像同步代码一样的风格,而不必手动管理 Promise 的链式调用
  • 主要优势在于简化了异步代码的编写和理解,使得处理异步操作更加直观和容易

async/await 怎么使用呢?

先将其分解:async 部分 和 await 部分

async 关键字开头 代表这是一个异步函数, 返回一个 Promise 对象,函数内部 return 语句返回的值,相当于执行了 Promise.then(res) 后回调参数的值。

例如:

async function foo () {
  return "Hello World";
}
foo().then((res) => console.log(res)) // "Hello World"

await 关键字 后面可接 Promise 对象任何具有 then() 方法的对象任何原始值异步函数Generator 对象

注意,await 不能单独出现,必须和async 一起出现。

接 Promise 对象示例:await 会等待该 Promise 对象状态变为 resolved(已完成)或 rejected(已拒绝),然后返回 Promise 的值(如果状态是 resolved)或抛出异常(如果状态是 rejected)

// 模拟promise
function promiseFunc () {
  return new Promise((resolve, reject) => {
    let rand = Math.random();
    rand > 0.5 ? resolve(rand) : reject(rand);
  })
}
// 直接接Promise 对象
async function foo () {
  return await promiseFunc();
}

foo().then(
  v => console.log("被resolve了",v),
  e => console.log("被reject了", e)
)

接任何具有then()方法的对象示例:具有类似 Promise 的行为,但并不是真正的 Promise。

class MyThenable {
  then(resolve, reject) {
    // 执行异步操作,最终调用 resolve 或 reject
    resolve("Hello");
  }
}
async function foo () {
  return await new MyThenable();
}
foo().then(
  v => console.log("被resolve了",v),
  e => console.log("被reject了", e)
)  // 被resolve了 Hello

接任何原始值示例:当 await 后面跟随一个原始值时,它会立即将该值解析为已完成的 Promise,并返回该值本身。

async function foo () {
  return await 22; // 等同于 Promise.resolve(22)
}
foo().then(
  v => console.log("被resolve了",v),
  e => console.log("被reject了", e)
)  // 被resolve了 22

接异步函数示例 : 在异步函数内部,可以使用 await 关键字等待另一个异步函数的执行结果

function promiseFunc () {
  return new Promise((resolve, reject) => {
    let rand = Math.random();
    rand > 0.5 ? resolve(rand) : reject(rand);
  })
}
async function foo () {
  return await promiseFunc();
}
async function bar () {
  return await foo();
}
bar().then(
  v => console.log("被resolve了",v),
  e => console.log("被reject了", e)
)
// 被reject了 0.23166145735495758

当async 函数里面有多个await的时候是怎么样的呢?

function promiseFunc () {
  return new Promise((resolve, reject) => {
    let rand = Math.random();
    rand > 0.5 ? resolve(rand) : reject(rand);
  })
}
async function foo () {
	const rst1 = await promiseFunc();
  const rst2 = await promiseFunc();
  return {
    rst1,
    rst2
  }
}
foo().then(res=> console.log(res)).catch(err => console.log(err));
// 情况一
// [0.9080063738596382, 0.5729561832964878]
// 情况二
// reject: 0.13659417848799338
// 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行.
// 如果想要保证后面的await 可以正常运行,可以使用 try catch
async function bar() {
  let rst = 0;
  try {
    rst = await promiseFunc();
  } catch (e) {
    console.log("e",e)
  }
  return await (`哈哈:${rst}`);
}
bar().then((res) => console.log(res)).catch((err) => console.log(err));
// 当promiseFunc抛出异常时的结果是:后面的await正常输出了
// e reject: 0.33490751710711564
// 哈哈:0

// 当然如果不喜欢写try catch ,还可以这么写:
async function f() {
  let rst = await promiseFunc().catch(function (err) {
    console.log(err);
  });
  return await (`哈哈:${rst}`)
}
// 当promiseFunc抛出异常时的结果如下:
// reject: 0.19910784450616492
// 哈哈:undefined

// 当resolve的时候结果如下:
// 哈哈:0.6107829192741085

看看现实使用中的场景?

  1. fetch 请求数据,当出现有先后顺序的异步操作时很方便的用同步的写法
import mjFetch from 'mj-fetch';

// 获取token的fetch请求,实际返回的是一个Promise对象
const fetchToken = (data) => {
  return mjFetch.get('/token', data).catch((err) => console.log(err));
}
// 获取用户信息的fetch请求,实际返回的是一个Promise对象
const fetchUserInfo = (data) => {
  return mjFetch.get('/user/info/yth', data).catch(err => window.alert("登录失败,请重新进入"));
}

// 定义获取用户信息的异步方法
const getUserInfo = async () => {
  const token = await fetchToken();
	const rst = await fetchUserInfo({ token });
}

// ========================== 如果不用 async/await ,而用 promise的方式,你可能要这么写
const getUserInfoPromise = () => {
  fetchToken().then((token) => {
    const rst = await fetchUserInfo({ token }); // 如果依赖多了,就可能变成回调地狱了
  })
}
  1. node中常见的async /await 用法,举个例子:文件目录监听变化文件并根据配置规则读取文件内容
const { readFile } = require("node:fs/promise");
const watch = require("node-watch");

const monitor = async (folder) => {
  const asyncFunc1 = readFile('SETTING_ONE.txt', { encoding: 'utf-8' });
  const asyncFunc2 = readFile('SETTING_TWO.txt', { encoding: 'utf-8' });
  const [cfg1, cfg2] = await Promise.all([asyncFunc1, asyncFunc2]); // 并行获取配置内容
  watch(folder, (evt, name) => {
    if (evt ==== 'update') {
      // readAlgc(name, cfg1, cfg2) 根据配置规则读取文件内容
    }
  })
}

// 开启监听
monitor('xx文件夹');

我们都知道Promise.resolve 、Promise.reject 是微任务,那么 async/await 中的 await 表达式是微任务吗?

先简单的介绍一下微任务、宏任务:

我们所知道的异步任务分为宏任务和微任务,微任务会被放入微任务队列,宏任务会被放入宏任务队列,在当前执行栈中,当执行栈为空时,会先检查当前的微任务队列,将所有微任务执行完毕后,再去执行下一个宏任务。

那么这些队列是什么,执行栈又是什么? 这就涉及到事件循环、调用堆栈和任务队列了。

事件循环:js中事件循环主要是用来处理js单线程下的异步编程问题。当执行一段js代码时,代码运行时创建的执行上下文,会被推入调用栈中 ,如果是异步任务,会移交给相应web api 去处理,并将处理完的回调事件,放入消息队列中,当主线程处理完调用栈的代码,事件循环就会不断地从消息队列中获取事件,并推入调用栈中执行。这个过程不断地循环,这种运行机制就是 Event Loop机制。

调用堆栈:存放的是执行上下文,先进后出。

任务队列:存放的是异步操作的回调函数,先进先出。

async/await 中的 await 关键字会创建一个微任务(microtask),当执行到 await 关键字时,它会暂停 async 函数的执行,并等待 Promise 对象的状态变更。

你理解了吗,来看看一道比较经典的面试题:

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}
setTimeout(function() {
  console.log('settimeout');
});
async1();
new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
})

// 从上往下看,当前执行栈 [global]
// 遇到setTimeout ,放入宏任务队列
// 执行async1(), 入栈 [global,async1()]  直接执行'async1 start',
// 遇到await async2 创建微任务并放入微任务队列async2-p ,执行 'async2', 因为没有等到async2执行返回结果,所以不执行后续的,直接跳出 async1 函数。
// 接着遇到 new Promise,直接执行'promise1', resolve()放入微任务队列
// 当前任务执行完毕,开始去执行微任务队列中的任务
// async2-p执行,接着运行它的下一行代码 'async1 end'
// resolve()执行,then回调触发 'promise2'
// 微任务队列执行完毕,开始执行宏队列
// 'settimeout'
// 所以最终的顺序是:"async1 start" 、"async2"、"promise1"、"async1 end" 、"promise2"、"settimeout"

// 主线程:"async1 start" 、"async2"、"promise1"
// 宏队列: [setTime cb()]
// 微队列: [async2-p, resolve()]

Async /Await 的各种声明方式参考:

一些术语说明:

AsyncFunctionBody:异步函数的函数体,与普通函数体类似,但允许在其中使用 await 表达式.
AwaitExpression:异步函数内部使用的 await 表达式

异步函数声明(AsyncFunctionDeclaration):

async function name () {AsyncFunctionBody}
// 匿名函数声明
async function () {AsyncFunctionBody}

异步函数表达式(AsyncFunctionExpression):

const asyncFunc = async function () { AsyncFunctionBody }

异步方法的定义形式(AsyncMethod),用于在类(Class)中定义异步方法。

async ClassElementName (...) { AsyncFunctionBody }

异步箭头函数定义(Async Arrow Function Definitions)

const asyncArrowFunc = async () => {}

Await 解析规则:

await xxPromise; // await 后面接一个Promise对象的表达式 (任何返回 Promise 对象的函数或表达式)
// await 会暂停异步函数的执行,等待 Promise 对象的解析结果。
// 在 AsyncFunctionBody 中
async function asyncFunction() {
  await someAsyncOperation();
}
// 在异步函数定义的形参中
async function asyncWithParams(awaitParam) {
  await awaitParam;
}
// 在模块(Module)中
export async function asyncModuleFunction() {
  await someAsyncModuleOperation();
}