系统性捕获某个网络请求中所产生的所有错误

发布时间 2023-11-27 16:45:29作者: 柯基与佩奇

开发中无论怎样都会产生网络请求,这样一来自然也就避免不了大量使用 then、catch 或 try catch 来捕获错误,而捕获错误的代码量是随着网络请求的增多而增多,那应该如何优雅的系统性捕获某个网络请求中所产生的所有错误呢?

首先最常用的两种处理网络请求的形式即 Promise 与 async(事实上很多请求库都是基于这两者的封装),使用 Promise 那必然要与 then、catch 挂钩,也就是说每个请求都对应一个 Promise 实例,然后通过该实例上对应的方法来完成对应的操作,这应该算是比较常用的一种形式了

但如果涉及嵌套请求,那可能还要不断的增加 then、catch 来完成需求,好了,现在可以使用看起来真的像同步编程的 async 来着手优化了,即 await promise,那这种情况下就根本不需要手动 then 了,但如果 await promise 抛出了错误呢?那恐怕不得不让 try catch 来帮忙了,而如果也是嵌套请求,那与 Promise 写法类似的问题又来了,有多少次请求难道就要多少次 try catch 吗?那这样看来的话,Promise 与 async 在面对这种屎山请求的时候确实有点心有余而力不足了

前言

前几天在优化数据库操作时,发现要不停 try catch,且操作数据库的代码越多,则 try catch 就越多,于是突发奇想,能不能封装一个工具类来实现智能化捕获错误呢?

一个令人头疼的需求

吉林的小明想去海南,但小明觉得旅途如此之长,不如先去山东,然后再去云南的视频发一下朋友圈,最后再去海南

查询吉林--山东--云南--海南的车票还有吗?

希望小明不要在车票上花费太多的钱,所以当小明出发时,需要知道开销是多少
如果没有的话,告诉小明是哪里的车票没有了

注意,当确定吉林-山东的车票未售空时才去查询山东-云南的车票是否已售空,并以此类推;因为这样的话,小明可以知道是哪个地方的车票没有了,并及时换乘

虽然吉林--山东--云南--海南的车票可以一次性查询完毕,但为了体现嵌套请求的复杂度,此处不讨论并发请求的情况,关于并发,可以使用 Promise.all
alt

先来细化题目,可以看到路线依次为:吉林-山东、山东-云南、云南-海南,也就分别对应三个请求,且这三个请求又是嵌套发出的。而每次发出的请求,最终都会有两种情况:请求成功/失败,请求成功则代表本轮次车票未售空,请求失败则代表本轮次车票已售空

之所以请求失败对应车票已售空,是为了模拟请求失败的情况,而不是通过返回一个标识来代表本轮次车票是否已售空

准备工作

请求函数,在下文中所指的请求函数就是 requestJS、requestSY、requestYH

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true];

// 查询 吉林-山东 的车票是否已售空的接口
const requestJS = () =>
  new Promise((res, rej) => {
    setTimeout(() => {
      // 请求成功(resolve)则代表车票未售空
      if (interface[0])
        return res({ ticket: true, price: 530, destination: "吉林-山东" });
      // 请求成功(rejected)则代表车票已售空
      rej({ ticket: false, destination: "吉林-山东" });
    }, 1000);
  });
// 查询 山东-云南 的车票是否已售空的接口
const requestSY = () =>
  new Promise((res, rej) => {
    setTimeout(() => {
      if (interface[1])
        return res({ ticket: true, price: 820, destination: "山东-云南" });
      rej({ ticket: false, destination: "山东-云南" });
    }, 1500);
  });
// 查询 云南-海南 的车票是否已售空的接口
const requestYH = () =>
  new Promise((res, rej) => {
    setTimeout(() => {
      if (interface[2])
        return res({ ticket: true, price: 1500, destination: "云南-海南" });
      rej({ ticket: false, destination: "云南-海南" });
    }, 2000);
  });

Promise

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true];

// 先查询吉林到山东
requestJS()
  .then(({ price: p1 }) => {
    console.log(`吉林-山东的车票未售空,价格是 ${p1} RMB`);
    // 如果吉林-山东的车票未售空,则继续查询山东-云南的车票
    requestSY()
      .then(({ price: p2 }) => {
        console.log(`山东-云南的车票未售空,价格是 ${p2} RMB`);
        // 如果山东-云南的车票未售空,则继续查询云南-海南的车票
        requestYH()
          .then(({ price: p3 }) => {
            console.log(`云南-海南的车票未售空,价格是 ${p3} RMB`);
            console.log(`本次旅途共计车费 ${p1 + p2 + p3} RMB`);
          })
          .catch(({ destination }) => {
            console.log(`来晚了,${destination}的车票已售空`);
          });
      })
      .catch(({ destination }) => {
        console.log(`来晚了,${destination}的车票已售空`);
      });
  })
  .catch(({ destination }) => {
    console.log(`来晚了,${destination}的车票已售空`);
  });

测试结果如下
alt

符合预期效果,现在来将第二次请求变为失败(即山东-云南请求失败)

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true];

再来看结果
alt

依然符合预期效果,但这种方式嵌套的层级太多,一不小心就会成为屎山的必备条件,必须优化一下
由于 then 会在请求成功时触发,catch 会在请求失败时触发,而无论是 then 或 catch 都会返回一个 Promise 实例(return this),也正是借助这个特性来实现 then 的链式调用
如果 then 方法没有返回值,则默认返回一个成功的 Promise 实例,而下面代码则手动为 then 指定了其需要返回的 Promise 实例。无论其中哪个 Promise 的状态更改为失败,都会被最后一个 catch 所捕获

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, false];

let acc = 0;
// 先查询吉林到山东
requestJS()
  .then(({ price: p1 }) => {
    acc += p1;
    console.log(`吉林-山东的车票未售空,价格是 ${p1} RMB`);
    // 如果吉林-山东的车票未售空,则继续查询山东-云南的车票
    return requestSY();
  })
  .then(({ price: p2 }) => {
    acc += p2;
    console.log(`山东-云南的车票未售空,价格是 ${p2} RMB`);
    // 如果山东-云南的车票未售空,则继续查询云南-海南的车票
    return requestYH();
  })
  .then(({ price: p3 }) => {
    // 能执行到这里,就说明前面所有请求都成功了
    acc += p3;
    console.log(`云南-海南的车票未售空,价格是 ${p3} RMB`);
    console.log(`本次旅途共计车费 ${acc} RMB`);
  })
  .catch(({ destination }) =>
    console.log(`来晚了,${destination}的车票已售空`)
  );

alt

可以看到经过优化后的 Promise 已经把屎山磨平了一点,美中不足的就是如果想要计算总共花费的车费,那么需要在外部额外声明一个 acc 用来统计数据,其实这种情况可以对请求车票数据的函数 requestJS 等来和每次 then 的返回值进行简单包装,但在此处,不想改动请求车票数据的函数体,至于为什么,继续往下看

async

来看 async 会怎么处理这种嵌套请求

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true];

const f = async () => {
  try {
    const js = await requestJS();
    console.log(`吉林-山东的车票未售空,价格是 ${js.price} RMB`);
    const sy = await requestSY();
    console.log(`山东-云南的车票未售空,价格是 ${sy.price} RMB`);
    const yh = await requestYH();
    console.log(`云南-海南的车票未售空,价格是 ${yh.price} RMB`);
    console.log(`本次旅途共计车费 ${js.price + sy.price + yh.price} RMB`);
  } catch ({ destination }) {
    console.log(`来晚了,${destination}的车票已售空`);
  }
};

f();

alt

其实 async 与上面 Promise 的第二种写法有异曲同工之妙,可以看做都是将所有成功的逻辑放在了一起,仅仅使用了一个 catch 便可以捕获所有错误

上面所讲到的 Promise 与 async 其实已经是很常见的一种写法了,但如果项目中存在第二种嵌套请求(比如先请求所在省份的天气,再请求所在县的天气)。如果放在 async 面前,想它一定会使用两个 f 函数,一个为查询小明车票,一个为查询天气,那这就避免不了要写两个 try catch 了

combine-async-error 心路历程

要解决一个问题,首先要明白解决它的意义何在。正是被这种不停地 catch 所困惑,所以才要想出更好的办法去优化它。于是就想着能不能封装一个函数来完成所有的 catch 操作呢?

封装之前

try catch 不能捕获异步错误

// 可以捕获
try {
  throw ReferenceError("对象 is not defined");
} catch (e) {
  console.log(e);
}
// 不可以捕获
try {
  setTimeout(() => {
    throw ReferenceError("对象 is defined");
  });
} catch (e) {
  console.log(e);
}

Generator

可以把 Generator 函数称作生成器,调用生成器函数会返回一个迭代器来控制这个生成器执行其代码,在生成器中可以使用 yield 关键字,理论上 yield 可以出现在任何能求值的地方,通过迭代器的 next 方法来确保生成器始终是可控的

const f = function* () {
  console.log(1);
  // 注意yield只能出现在Gerenator函数中
  // 如果将yield写在了回调里,请一定要确认这个回调是一个生成器函数
  yield;
  console.log(2);
};
f().next();

// 1

async

async 函数在执行时,遇到 await 会交出“线程”,转而去执行其它任务,且 await 总是会异步求值

const f = async () => {
  console.log(1);
  await "鲨鱼辣椒";
  console.log(3);
};
f();
console.log(2);

// 1 2 3

让 await 永远不要抛出错误

让 await 永远不要抛出错误,这也是最重要的前提

// getInfo为获取车票信息的功能函数
const getInfo = async () => {
  try {
    const result = await requestJS();
    return result;
  } catch (e) {
    return e;
  }
};

await 右边是获取吉林-山东车票信息的函数 requestJS,该函数会返回一个 promise 对象,当这个 promise 对象的状态为成功时,await 会把成功的值赋给 result,而当失败时,会直接抛出错误,一般会在 await 外包裹一层 try catch 来捕获可能出现的错误,那能不能不让 await 抛出错误呢?
只需要封装一下 await 关键字即可

保证不抛出错误

// noErrorAwait负责拿到成功或失败的值,并保证永远不会抛出错误!
const noErrorAwait = async (f) => {
  try {
    const r = await f();
    return { flag: true, data: r };
  } catch (e) {
    return { flag: false, data: e };
  }
};

const getInfo = () => {
  const result = noErrorAwait(requestJS);
  return result;
};

在 noErrorAwait 的 catch 里请不要进行一些副作用操作

有了 noErrorAwait 的加持,getInfo 可以不再是一个 async 函数了,但此时的 getInfo 仍会返回一个 promise 对象,这是因为 noErrorAwait 是 async 函数的缘故。封装到这里,noErrorAwait 已经实现了它的第一个特点——保证不抛出错误,现在来把 getInfo 补全

const noErrorAwait = async (f) => {
  try {
    const r = await f(); // (A)
    return { flag: true, data: r };
  } catch (e) {
    return { flag: false, data: e };
  }
};

const getInfo = () => {
  const js = noErrorAwait(requestJS); // (B)
  console.log(`吉林-山东的车票未售空,价格是 ${js.data.price} RMB`);
  const sy = noErrorAwait(requestSY); // (C)
  console.log(`山东-云南的车票未售空,价格是 ${sy.data.price} RMB`);
  const yh = noErrorAwait(requestYH); // (D)
  console.log(`云南-海南的车票未售空,价格是 ${yh.data.price} RMB`);
  console.log(`本次旅途共计车费 ${js.price + sy.price + yh.price}`);
};

分别为(B)、(C)、(D)所对应的请求函数都套上了一层 noErrorAwait,正是由于这种缘故,可以在 getInfo 中始终确保(B)、(C)、(D)下的请求函数不会报错,但致命的问题也随之到来,getInfo 会确保请求函数是顺序执行的吗?

仔细看一遍就会发现 getInfo 是不负责顺序执行的,甚至可能会报错。这是因为 noErrorAwait 中 await 关键字的缘故,现在手动执行一下分析原因

  1. 调用 getInfo
  2. 调用 noErrorAwait 并传递参数 requestJS
  3. 来到 noErrorAwait 中,由于 noErrorAwait 是 async 函数,所以会返回一个 promise 对象
  4. 执行 await f(),这个 f 就是 requestJS,由于 requestJS 是一个异步任务,所以交出本次“线程”,也就是从(A)跳到(B)的下方,打印 js.data.price,结果发现抛出了 TypeError
  5. 抛出 TypeError 的原因是因为(B)的变量 js 是一个初始化状态的 promise 对象,所以说访问初始化中的数据怎么可能不报错!

那问题来了,noErrorAwait 只负责让所有的请求函数都不抛出错误,但它并不能确保所有请求函数是按顺序执行的,如何才能让它们按照顺序执行呢?

难不成又要把 getInfo 变回 async 函数,然后再通过 await noErrorAwait(...)的形式来确保所有请求函数是按照顺序执行的,如果真的使用这种方式,那 await noErrorAwait(...)如果抛出了错误,谁来捕获呢?总不能在它外面再套一层 noErrorAwait 吧

保证顺序执行

这个想法实现到这里,其实已经出现了很大的问题了——“保证不抛出错误”和“顺序执行”不能同时成立,但也不能遇到 bug 就关机。Generator,由于生成器是可控的,只需要在上一次请求完成时,调用 next 发起下一次请求,就可以解决

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true];

const noErrorAwait = async (f) => {
  try {
    const r = await f();
    generator.next({ flag: true, data: r });
  } catch (e) {
    return { flag: false, data: e };
  }
};

const getInfo = function* () {
  const js = yield noErrorAwait(requestJS);
  console.log(`吉林-山东的车票未售空,价格是 ${js.data.price} RMB`);
  const sy = yield noErrorAwait(requestSY);
  console.log(`山东-云南的车票未售空,价格是 ${sy.data.price} RMB`);
  const yh = yield noErrorAwait(requestYH);
  console.log(`云南-海南的车票未售空,价格是 ${yh.data.price} RMB`);
  console.log(
    `本次旅途共计车费 ${js.data.price + sy.data.price + yh.data.price}`
  );
};

const generator = getInfo();
generator.next();

先来看测试结果
alt

当请求全部成功时,所有数据都拿到了,不得不说,这一切都要归功于 yield 关键字
当 noErrorAwait 感知到请求函数成功时,会调用 next,从而推动嵌套请求的发起,而且也不用担心生成器在什么时候执行完,因为一个 noErrorAwait 总会对应着一次 next,这样一来 getInfo 就差不多已经在掌控之中了.
但有个致命的问题就是:noErrorAwait 感知到错误时,应该如何处理?如果继续调用 next,那就与不用生成器没有区别了,因为始终都会顺序执行,解决办法就是传递一个函数,在 noErrorAwait 感知到错误时调用该函数,并且把出错的请求函数之前的所有请求结果全部传递进去,这样当这个回调执行时,便代表某一个请求函数抛出了错误

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true];

// 存储每次的请求结果
const result = [];
// 失败的回调(不要关心callback定义在哪里,以及如何传递)
const callback = (...args) =>
  console.log("某个请求出错了,前面收到的结果是", ...args); // (A)
const noErrorAwait = async (f) => {
  try {
    const r = await f();
    const args = { flag: true, data: r };
    result.push(args);
    generator.next(args);
  } catch (e) {
    const args = { flag: false, data: e };
    result.push(args);
    callback(result);
    return args;
  }
};

const getInfo = function* () {
  // (B)
  const js = yield noErrorAwait(requestJS);
  console.log(`吉林-山东的车票未售空,价格是 ${js.data.price} RMB`);
  const sy = yield noErrorAwait(requestSY);
  console.log(`山东-云南的车票未售空,价格是 ${sy.data.price} RMB`);
  const yh = yield noErrorAwait(requestYH);
  console.log(`云南-海南的车票未售空,价格是 ${yh.data.price} RMB`);
  console.log(
    `本次旅途共计车费 ${js.data.price + sy.data.price + yh.data.price}`
  );
};

const generator = getInfo(); // (C)
generator.next(); // (D)

通过测试可以发现当第二个请求函数抛出了错误时,noErrorAwait 可以完全捕获,并及时通过 callback 向用户返回了数据
alt

这样就实现了一个功能较为齐全的处理嵌套请求的函数了,但仔细看看就会发现,代码中的(A)、(B)、(C)、(D)(包括(B)中的所有 yield)都是由用户自定义的

开始封装

封装一个 combineAsyncError 函数,这个函数会完成所有的逻辑处理及调度,而用户则只需要传递请求函数即可

combineAsyncError 即字面意思,捕获异步错误,当然它也可以捕获同步错误

使用形式

const combineAsyncError = (tasks) => {};
const getInfo = [requestJS, requestSY, requestYH];

combineAsyncError(getInfo).then((data) => {
  console.log("请求结果为:", data);
});

combineAsyncError 接收一个由请求函数所构成的数组,该函数会返回一个 Promise 对象,其 then 方法被执行时,就代表嵌套请求结束了(有可能因为成功而结束,亦有可能因为失败而结束),不过不要担心,因为 data 的值始终为{ result, error },如果 error 存在则代表请求失败,反之成功

完成 combineAsyncError 的返回值

const combineAsyncError = (tasks) => {
  return new Promise((res) => handler(res));
};

当调用 res 时,会通知当前的 Promise 实例去执行它的 then 方法,而 res 也正是杀手锏,只需在请求失败或全部请求成功时调用 res,这样 then 就会知道嵌套请求的逻辑执行完毕

combineAsyncError 的初始化工作

在 handler 中完成处理请求函数的逻辑。也就是操作 Generator 函数,既然这里要使用生成器,那就很有必要做一下初始化工作

const combineAsyncError = (tasks) => {
  const doGlide = {
    node: null, // 生成器节点
    out: null, // 结束请求函数的执行
    times: 0, // 表示执行的次数
    data: {
      // data为返回的最终数据
      result: [],
      error: null,
    },
  };
  const handler = (res) => {};
  return new Promise((res) => handler(res));
};

doGlide 相当于一个公共区域(也可以理解为原型对象),把一些值和数据存放在这个公共区域中,其它人可以通过这个公共区域来访问这里面的值和数据

在 handler 中使用 Generator

初始化完毕,现在所有的值和数据都找到存放的地方了,接下来在 handler 中使用生成器

const combineAsyncError = (tasks) => {
  const doGlide = {};
  const handler = (res) => {
    doGlide.out = res;
    // 预先定义好生成器
    doGlide.node = (function* () {
      const { out, data } = doGlide;
      const len = tasks.length;
      // yield把循环带回了JavaScript编程的世界
      while (doGlide.times < len) yield noErrorAwait(tasks[doGlide.times++]);
      // 全部请求成功(生成器执行完毕)时,返回数据
      out(data);
    })();
    doGlide.node.next();
  };
  return new Promise((res) => handler(res));
};

把 res 赋值给 doGlide.out,调用 out 就是调用 res,而调用 res 就代表本次处理完成(可以理解成 out 对应了一个 then 方法)。把 Generator 生成的迭代器交给 doGlide.node,并先在本地启动一下生成器 doGlide.node.next(),这个时候会进入 while,然后执行 noErrorAwait(tasks[doGlide.times++]),发出执行 noErrorAwait(...)的命令后,noErrorAwait 会被调用,且 while 会在此时变为可控的循环,因为 noErrorAwait 是一个异步函数,只有当 yield 得到具体的值时才会执行下一次循环(换句话说,yield 得到了具体的值,那就代表本轮循环完成),而 yield 有没有值其实无所谓,只是利用它的特性来把循环变为可控的而已

扩展 noErrorAwait

至此,所有的准备工作其实都已完备,就差 noErrorAwait 来完成整体的调度了

const combineAsyncError = (tasks) => {
  const doGlide = {};
  const noErrorAwait = async (f) => {
    try {
      // 执行请求函数
      const r = await f();
      // 追加数据
      doGlide.data.result.push({ flag: true, data: r });
      // 请求成功时继续执行生成器
      doGlide.node.next();
    } catch (e) {
      doGlide.data.error = e;
      // 当某个请求函数失败时,立即终止函数执行并返回数据
      doGlide.out(doGlide.data);
    }
  };
  const handler = (res) => {};
  return new Promise((res) => handler(res));
};

在 noErrorAwait 这个 async 函数中,使用 try catch 来保证每一次请求函数执行时都不会抛出错误,当请求成功时,追加请求成功的数据,并且继续执行生成器,而生成器执行完毕,也就代表 while 执行完毕,所以 out(data)实则是结束了整个 combineAsyncError 函数;而当请求失败时,则赋予 error 实际的值,并且执行 doGlide.out 来向用户返回所有值
至此,一个简单的 combine-async-error 函数便封装完毕了,现在通过两种情况进行测试

  1. 请求函数全部成功
// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, true, true];

const getInfo = [requestJS, requestSY, requestYH];

combineAsyncError(getInfo).then((data) => {
  console.log("请求结果为:", data);
});
  1. 某一个请求函数抛出错误
// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true];

const getInfo = [requestJS, requestSY, requestYH];

combineAsyncError(getInfo).then((data) => {
  console.log("请求结果为:", data);
});

比较三种形式(Promise、async、combine-async-error)

现在来比较一下三种形式,三种形式统一使用下面的请求结果

// 标识每次请求的成功与否(吉林-山东、山东-云南、云南-海南)
const interface = [true, false, true];
  1. Promise
let acc = 0;
// 先查询吉林到山东
requestJS()
  .then(({ price: p1 }) => {
    acc += p1;
    console.log(`吉林-山东的车票未售空,价格是 ${p1} RMB`);
    // 如果吉林-山东的车票未售空,则继续查询山东-云南的车票
    return requestSY();
  })
  .then(({ price: p2 }) => {
    acc += p2;
    console.log(`山东-云南的车票未售空,价格是 ${p2} RMB`);
    // 如果山东-云南的车票未售空,则继续查询云南-海南的车票
    return requestYH();
  })
  .then(({ price: p3 }) => {
    // 能执行到这里,就说明前面所有请求都成功了
    acc += p3;
    console.log(`云南-海南的车票未售空,价格是 ${p3} RMB`);
    console.log(`本次旅途共计车费 ${acc} RMB`);
  })
  .catch(({ destination }) =>
    console.log(`来晚了,${destination}的车票已售空`)
  );
  1. async
const f = async () => {
  try {
    const js = await requestJS();
    console.log(`吉林-山东的车票未售空,价格是 ${js.price} RMB`);
    const sy = await requestSY();
    console.log(`山东-云南的车票未售空,价格是 ${sy.price} RMB`);
    const yh = await requestYH();
    console.log(`云南-海南的车票未售空,价格是 ${yh.price} RMB`);
    console.log(`本次旅途共计车费 ${js.price + sy.price + yh.price} RMB`);
  } catch ({ destination }) {
    console.log(`来晚了,${destination}的车票已售空`);
  }
};

f();
  1. combine-async-error
const getInfo = [requestJS, requestSY, requestYH];
combineAsyncError(getInfo).then(({ result, error }) => {
  result.forEach(({ data }) =>
    console.log(`${data.destination}的车票未售空,价格是 ${data.price} RMB`)
  );
  if (error) console.log(`来晚了,${error.destination}的车票已售空`);
});

可以看到 combine-async-error 这种智能捕获错误的方式确实优雅,无论多少次嵌套请求,始终只需要一个 then 便可以轻松胜任所有工作,并且使用 combine-async-error 的形式也很简洁,根本不需要编写复杂的嵌套层级,在使用之前也不需要进行其它令人头疼的操作

其他办法

函数式编程

实现方式:

const pipePromise = (result, asyncFns) =>
  asyncFns.reduce((acc, cur) => acc.then(cur), Promise.resolve(result));
pipePromise([], [requestJS, requestSY, requestYH])
  .then((data) => console.log("请求结果为:", data))
  .catch(console.warn);

轮子 await-to-js

常用

Promise.allSettled()

Promise.allSettled() 方法返回一个在所有给定的 promise 都已经 fulfilled 或 rejected 后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。

Promise.allSettled() 可用于并行执行独立的异步操作,并收集这些操作的结果。
该函数接受一个 promise 数组(通常是一个可迭代对象)作为参数:

const statusesPromise = Promise.allSettled(promises);

当所有的输入 promises 都被 fulfilled 或 rejected 时,statusesPromise 会解析为一个具有它们状态的数组

  1. { status: 'fulfilled', value: value } — 如果对应的 promise 已经 fulfilled
  2. 或者 {status: 'rejected', reason: reason} 如果相应的 promise 已经被 rejected

在解析所有 promises 之后,可以使用 then 语法提取它们的状态:

statusesPromise.then(statuses => {
 statuses; // [{ status: '...', value: '...' }, ...]
});
``
或者使用 async/await 语法:
const statuses = await statusesPromise;
statuses; // [{ status: '...', value: '...' }, ...]

Promise.allSettled(promises)可以并行地运行 promise,并将状态(fulfilled 或 reject)收集到一个聚合数组中。
Promise.allSettled(...)在需要执行平行和独立的异步操作并收集所有结果时非常有效,即使某些异步操作可能失败。