前端知库

怎样手写 Promise.race、Promise.allSettled 和 Promise.any?

三者都返回一个新 Promise,遍历输入并用 Promise.resolve 包装每一项。race 跟随第一个敲定的结果;allSettled 等全部敲定后按输入顺序返回状态对象;any 取第一个成功的值,全部失败时用 AggregateError 拒绝。空数组时 race 永远 pending,allSettled 兑现为空数组,any 以 AggregateError 拒绝。

参考答案

简答

给接口请求加超时,常见写法是 Promise.race([fetch(url), timeout(3000)]),谁先有结果就用谁。手写 race、allSettled、any 的思路都一样:返回一个新 Promise,遍历输入,用 Promise.resolve 包装每一项,再决定什么时候 resolve 或 reject。

三者的区别在于等什么。race 跟随第一个敲定(成功或失败)的结果;allSettled 等所有项都敲定,按输入顺序返回 { status, value } 或 { status, reason },自己永远不会失败;any 取第一个成功的值,只有全部失败时才拒绝,拒绝原因是 AggregateError,它的 errors 属性按输入顺序放着每个失败原因。

空数组是常被问到的边界,三者各不相同。MDN 写明:race([]) 永远处于 pending;allSettled([]) 直接兑现为 [];any([]) 直接以 AggregateError 拒绝。

要注意,这几个方法敲定以后不会取消其他还在进行的操作。race 做超时只是不再等慢的那个请求,请求本身还在跑,真要中断得配合 AbortController。

三个方法的实现

下面的实现重点看两处:怎样按输入顺序保存结果,以及怎样判断“全部完成”。

JavaScript
function race(iterable) {
  return new Promise((resolve, reject) => {
    for (const item of iterable) {
      Promise.resolve(item).then(resolve, reject);
    }
  });
}

function allSettled(iterable) {
  return new Promise((resolve) => {
    const results = [];
    let count = 0;
    let index = 0;
    for (const item of iterable) {
      const i = index++;
      Promise.resolve(item).then(
        (value) => {
          results[i] = { status: 'fulfilled', value };
          if (++count === index) resolve(results);
        },
        (reason) => {
          results[i] = { status: 'rejected', reason };
          if (++count === index) resolve(results);
        },
      );
    }
    if (index === 0) resolve(results);
  });
}

function any(iterable) {
  return new Promise((resolve, reject) => {
    const errors = [];
    let count = 0;
    let index = 0;
    for (const item of iterable) {
      const i = index++;
      Promise.resolve(item).then(resolve, (reason) => {
        errors[i] = reason;
        if (++count === index) {
          reject(new AggregateError(errors, 'All promises were rejected'));
        }
      });
    }
    if (index === 0) {
      reject(new AggregateError(errors, 'All promises were rejected'));
    }
  });
}

const delay = (ms, value, fail = false) =>
  new Promise((resolve, reject) => setTimeout(() => (fail ? reject(value) : resolve(value)), ms));

async function main() {
  console.log(await race([delay(30, '慢'), delay(10, '快')])); // 快
  console.log(await allSettled([delay(20, 'A'), delay(10, '坏', true), 3]));
  // [
  //   { status: 'fulfilled', value: 'A' },
  //   { status: 'rejected', reason: '坏' },
  //   { status: 'fulfilled', value: 3 }
  // ]
  console.log(await any([delay(10, 'x', true), delay(20, 'y')])); // y
  try {
    await any([delay(20, 'e1', true), delay(10, 'e2', true)]);
  } catch (e) {
    console.log(e instanceof AggregateError, e.errors); // true [ 'e1', 'e2' ]
  }
  console.log(await allSettled([])); // []
  try {
    await any([]);
  } catch (e) {
    console.log(e.errors.length); // 0
  }
  const winner = await Promise.race([race([]), delay(10, '空数组的 race 一直没结果')]);
  console.log(winner); // 空数组的 race 一直没结果
}

main();

为什么 race 可以把 resolve 和 reject 直接传给每一项?因为一个 Promise 只能敲定一次。第一个有结果的项调用了 resolve 或 reject 以后,后面再调用都不起作用,所以不需要额外的标记。

为什么 allSettled 的结果里,10 毫秒就失败的“坏”排在第二位,而不是第一位?因为结果是按下标 results[i] 写进去的,不是按完成先后 push。MDN 说结果数组的顺序和输入顺序一致,any 的 errors 也一样,所以例子里 e2 先失败,errors 仍然是 ['e1', 'e2']。

count 和 index 比较,是因为输入是可迭代对象,不一定有 length,只能边遍历边计数。遍历是同步完成的,而回调最早也要到微任务里才执行,所以回调运行时 index 已经是总数了。

Promise.resolve(item) 让普通值也能参与。例子里的 3 不是 Promise,包装以后马上兑现,结果是 { status: 'fulfilled', value: 3 }。

空数组为什么各不相同

for 循环一次都不执行时,race 里没有任何地方调用 resolve 或 reject,返回的 Promise 就永远是 pending。这和 MDN 的描述一致:“An empty iterable causes the returned promise to be forever pending”。例子里用原生 Promise.race 给它配了一个 10 毫秒的对手,最后胜出的是对手。

allSettled 和 any 需要单独处理 index === 0。allSettled 没有要等的项,直接兑现为空数组;any 没有任何一项能成功,也就等于“全部失败”,MDN 写明这时返回的 Promise 已经是拒绝状态,原因是一个 errors 为空数组的 AggregateError。

这里有一个细节和原生不同。MDN 说 allSettled 和 any 只有在输入为空时才同步敲定,其他情况都是异步敲定;race 则总是异步敲定。上面的实现也符合这一点,因为非空时结果都在 then 回调里产生。

常见追问

怎样用 race 实现请求超时?

写一个 timeout(ms) 返回在 ms 毫秒后拒绝的 Promise,和请求一起传给 race。要注意 race 只决定用谁的结果,慢的请求不会被取消。真要中断请求,可以给 fetch 传 AbortSignal.timeout(ms) 或者自己的 AbortController.signal。

allSettled 和 all 有什么区别?

all 遇到第一个失败就立刻拒绝,适合“缺一不可”的场景;allSettled 永远等到全部敲定,适合批量请求后分别处理成功和失败的场景。四个组合方法的选择可以看《Promise.all、allSettled、race 和 any 如何选择?》。

any 和 race 有什么区别?

race 跟随第一个敲定的结果,第一个失败了它就失败;any 会忽略失败,继续等下一个成功的,只有全部失败才拒绝。比如从多个镜像地址取同一份资源,用 any 能拿到最快的那个可用结果。