轻松搞懂 Promise.all / race / any / allSettled 之间的区别

JavaScript 里 Promise 有四个用于并发调度的静态方法——Promise.allPromise.racePromise.anyPromise.allSettled

很多人搞不清楚这几个方法的区别,今天我们就来简单梳理一下。

在正式开始之前,我们先来想一个真实场景。


一顿年夜饭

假设除夕夜你要张罗一桌年夜饭,同时准备三道菜:

  1. 炖排骨(砂锅慢炖,要 60 分钟)
  2. 蒸鲈鱼(上锅蒸,要 15 分钟)
  3. 拍黄瓜(切菜拌蒜,要 5 分钟)

这三件事可以同时进行——这就是异步。你不会傻到先炖完排骨再蒸鱼再拍黄瓜,那样等开始吃年夜饭的时候已经是半夜了。

每道菜的准备过程就是一个 Promise:它最终会完成(fulfilled),也可能搞砸(rejected)——排骨炖干了,鱼蒸老了,黄瓜拌咸了。

现在问题是:你到底怎么等这三道菜?

这取决于你是什么样的厨师。而这四种等待方式,正好对应四个 Promise 方法。


第一种厨师:完美主义者 → Promise.all

这位厨师说:

"三道菜全部成功,我才能上桌。任何一道搞砸了,今晚的年夜饭就散了。"

这就是 Promise.all

const dinner = Promise.all([
  stewRibs(),      // 炖排骨
  steamFish(),     // 蒸鲈鱼
  tossCucumber()   // 拍黄瓜
]);

dinner
  .then(([ribs, fish, cucumber]) => {
    console.log("开饭了!三道菜全部就位。");
  })
  .catch(error => {
    console.log("年夜饭散了——" + error);
  });

它的规则很简单:

  • 三道菜全部成功 → 你拿到一个数组 [排骨, 鲈鱼, 黄瓜],顺序和你传进去的顺序一模一样。注意,是你传进去的顺序,不是完成的顺序——拍黄瓜 5 分钟就好了,排骨 60 分钟才好,但结果数组里排骨排第一。
  • 任何一道菜失败 → 整个 Promise.all 立刻 reject,带上那个失败的原因。其他菜还在做?它不管了。

这里有个关键细节很多人没想清楚:"立刻 reject" 不等于 "取消其他任务"。

Promise 一旦开始就没有"取消"这回事。排骨还在砂锅里炖着呢,只是厨师不再等它了。如果排骨最终炖干了,那个 rejection 会被静默吞掉(在 Node.js 里可能触发 unhandledRejection 警告)。这就像你宣布年夜饭散了,但灶上的火还开着——你得自己记得去关。

一句话总结 Promise.all:全部成功才算成功,一个失败就全盘否定。


第二种厨师:急性子 → Promise.race

这位厨师说:

"我等不了那么多。谁先完成——不管是做好了还是搞砸了——我就认那个结果。"

这就是 Promise.race

想象一场真正的赛跑。跑道上三个人同时出发。第一个冲过终点线的人,赢了。但如果第一个人在中途摔倒了(reject),那他也是"第一个完成的"——比赛结束,结果是"输了"。

const result = Promise.race([
  fetchFromCache(),    // 从缓存读,快
  fetchFromNetwork(),  // 从网络读,慢
  timeout(5000)        // 5秒超时
]);

result
  .then(value => {
    console.log("第一个回来的赢了:" + value);
  })
  .catch(error => {
    console.log("第一个回来的搞砸了:" + error);
  });

它的规则更简单:

  • 第一个 promise 完成(fulfilled)→ Promise.race 就 fulfilled,带上那个值。
  • 第一个 promise 失败(rejected)→ Promise.race 就 rejected,带上那个原因。
  • 不管是成功还是失败,第一个定局的(settled)说了算,其他的全部忽略。

最常见的用法就是上面这种——竞速。缓存和网络同时请求,谁先回来用谁。再加一个超时 promise,如果 5 秒内谁都没回来,超时 promise 先 reject,整个 race 就失败了。

这里也有一个容易忽略的点:race 不是"第一个成功的"。是"第一个有结果的"。如果第一个有结果是失败,那就是失败。如果你要的是"第一个成功的",那不是 race,那是另一个故事了——你得自己写。

一句话总结 Promise.race:第一个定局的定胜负,不论成败。


第三种厨师:乐观主义者 → Promise.any

这位厨师说:

"只要有一道菜做好了,我就能上桌。全搞砸了,那才是真的完了。"

这就是 Promise.any

你在 race 那里看到了——第一个有结果的说了算,不管成败。但有时候你要的不是"第一个有结果的",你要的是"第一个成功的"。比如你同时向三个服务器发请求,哪个先返回有效数据就用哪个。一个服务器报错了?无所谓,等下一个。只有三个全挂了你才认输。

const data = Promise.any([
  fetchFromServer1(),   // 主服务器
  fetchFromServer2(),   // 备用服务器
  fetchFromServer3()    // 备用服务器
]);

data
  .then(value => {
    console.log("第一个成功的数据:" + value);
  })
  .catch(errors => {
    console.log("全部失败——", errors);  // AggregateError
  });

它的规则:

  • 第一个 promise 成功(fulfilled)→ Promise.any 就 fulfilled,带上那个值。其他的不等了。
  • 只有全部失败Promise.any 才 reject,而且 reject 的不是一个普通错误,是一个 AggregateError——把所有失败原因打包在一起的错误。
  • 某些先失败了?不管。继续等。 直到有一个成功,或者全部失败。

注意它和 race 的区别——这是最容易搞混的一对:

  • race:第一个有结果的(成功或失败)说了算。
  • any:第一个成功的说了算。失败的不算数,继续等。

一个微妙但关键的细节:any忽略 rejection直到有 fulfilled 出现。这意味着如果有三个 promise,第一个立刻 reject 了,any 不会立刻 reject——它会继续等另外两个。只有在所有 promise 都 reject 之后,它才打包成一个 AggregateError 抛出来。

这是 ES2021 才加入的方法。你可能觉得"这不就是把 race 加个成功过滤吗?"——对,从行为上看确实如此。但从语义上看,它解决的是一个完全不同的问题:容错。race 是竞速,any 是冗余备份。

一句话总结 Promise.any:第一个成功的赢,全部失败才算输。


第四种厨师:记录员 → Promise.allSettled

这位厨师说:

"三道菜我都要等。做好的记下来做好了,搞砸的记下来搞砸了。我要的是全部的记录,不是一个最终结论。"

这就是 Promise.allSettled

想象你在做一份问卷调查。你发了 100 份问卷出去。有人认真填了,有人拒答了,有人填了一半放弃了。但你作为调查员,你要的不是"只要所有人都认真填了我才看"——你要的是所有人的反馈,不管是什么反馈。

const report = Promise.allSettled([
  stewRibs(),
  steamFish(),
  tossCucumber()
]);

report.then(results => {
  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`第 ${index + 1} 道菜做好了:${result.value}`);
    } else {
      console.log(`第 ${index + 1} 道菜搞砸了:${result.reason}`);
    }
  });
});

它的规则:

  • 等所有 promise 全部 settle(不管是 fulfilled 还是 rejected)。
  • 永远不会 reject。永远不会。
  • 返回一个数组,每个元素是 { status, value }{ status, reason }
    • 成功的:{ status: "fulfilled", value: 结果 }
    • 失败的:{ status: "rejected", reason: 失败原因 }

这是 2020 年才加入标准的方法(ES2020),比另外两个晚了很多年。为什么?因为人们在实践中发现,很多时候你不想要"一个失败就全盘放弃"的行为。你群发 100 条消息,3 条发送失败了,你不想整批都报错——你想知道哪 3 条失败了,然后重发。这就是 allSettled 存在的意义。

一句话总结 Promise.allSettled:全部等完,成败都记,永不报错。


把四个放在一起看

好,现在我们有了物理图像。让我把四个厨师请到同一张八仙桌前:

Promise.all Promise.race Promise.any Promise.allSettled
等什么 全部完成 第一个完成 第一个成功 全部完成
成功条件 全部成功 第一个成功的成功(但第一个失败的也算"完成") 第一个成功的 永远成功
失败条件 任何一个失败 第一个失败的失败 全部失败 永远不失败
返回什么 结果数组 第一个定局的结果 第一个成功的结果 状态记录数组
会不会 reject 会(全部失败时) 不会
reject 带什么 第一个失败原因 第一个失败原因 AggregateError(全部原因)
什么时候用 必须全部成功才有意义 只要最快的一个 容错备份,只要一个成功 每个结果都要知道

你看,这四个方法的区别不在于"语法",而在于你对失败的容忍度

  • all:零容忍。一个失败,全部作废。
  • race:无所谓成败,只要最快。
  • any:失败可以忽略,只要有一个成功。
  • allSettled:全容忍。失败也要,我全都要。

一个让你真正理解的问题

我现在问你一个问题。你不用回答我,你自己想想。

如果 Promise.all 里的某一个 promise 失败了,其他 promise 还在跑吗?

如果你犹豫了,说明你还在"知道名字"的阶段。

答案是:还在跑。 Promise 没有取消机制。Promise.all 只是不再等它们了,但它们还在后台执行。排骨还在砂锅里炖着呢。

这个问题为什么重要?因为它揭示了 Promise 的一个本质特性——Promise 是不可取消的。 一旦创建,它就会跑到 settle 为止。Promise.all 的"快速失败"只是在"等待"层面失败了,不是在"执行"层面失败了。这两个层面的区别,恰恰是很多人 bug 的来源。

如果你真想要"一个失败就取消其他的"行为,你需要用 AbortController——但那是另一回事了,今天不展开。


最后一件事:别只记结论

很多人看完这种文章会说"哦我懂了,all 是全部成功,race 是第一个,any 是第一个成功的,allSettled 是全部等完"。

不,你只是记住了结论。

真正理解的方式是:你能不能自己重新造出这四个方法?

让我给你看 Promise.all 的本质——它不过就是这么个东西:

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = new Array(promises.length);
    let completed = 0;

    if (promises.length === 0) {
      resolve(results);
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(value => {
        results[index] = value;    // 按原始位置存放
        completed++;
        if (completed === promises.length) {
          resolve(results);        // 全部成功才 resolve
        }
      }, reject);                  // 任何一个失败,直接 reject
    });
  });
}

看到没有?核心逻辑就两条:

  1. 用一个计数器,每完成一个加一,等到全部完成。
  2. 任何一个失败,直接调 reject,后面的不管了。

Promise.race 更简单——甚至没有计数器:

function myPromiseRace(promises) {
  return new Promise((resolve, reject) => {
    promises.forEach(promise => {
      Promise.resolve(promise).then(resolve, reject);
      // 第一个调用 resolve 或 reject 的,就定了
      // 后面的调用会被 Promise 忽略——Promise 只 settle 一次
    });
  });
}

它的精妙之处在于利用了 Promise 的一个基本特性:一个 Promise 只能被 settle 一次。 第一个调用 resolvereject 的人赢了,后面的调用自动失效。你不需要写任何"判断是否已经完成"的逻辑——Promise 帮你做了。

Promise.any 则是 race 和 all 的混合体——成功的行为像 race,失败的行为像 all 的反面:

function myPromiseAny(promises) {
  return new Promise((resolve, reject) => {
    const errors = new Array(promises.length);
    let rejectedCount = 0;

    if (promises.length === 0) {
      reject(new AggregateError([], "All promises were rejected"));
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        resolve,  // 第一个成功的,直接 resolve——和 race 一样
        reason => {
          errors[index] = reason;      // 记录失败原因
          rejectedCount++;
          if (rejectedCount === promises.length) {
            // 全部失败了,打包成 AggregateError
            reject(new AggregateError(errors, "All promises were rejected"));
          }
        }
      );
    });
  });
}

看到没有?any 的成功分支直接调 resolve——跟 race 一模一样,第一个成功就赢。但失败分支不一样:它不立刻 reject,而是计数。只有所有 promise 都失败了,它才把错误打包成一个 AggregateError 抛出去。这正是"第一个成功的赢,全部失败才算输"的字面翻译。

Promise.allSettled 则是把两个都等完:

function myPromiseAllSettled(promises) {
  return new Promise(resolve => {  // 注意:只有 resolve,没有 reject
    const results = new Array(promises.length);
    let completed = 0;

    if (promises.length === 0) {
      resolve(results);
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        value => {
          results[index] = { status: "fulfilled", value };
          completed++;
          if (completed === promises.length) resolve(results);
        },
        reason => {
          results[index] = { status: "rejected", reason };
          completed++;
          if (completed === promises.length) resolve(results);
        }
      );
    });
  });
}

看到区别了吗?allSettled 的成功和失败分支都做同一件事——记录结果,计数器加一,检查是否全部完成。它永远走 resolve,因为对它来说,失败也是一种"结果",不是一种"错误"。


所以,到底选哪个?

不要问"我应该用哪个"。问自己一个问题:

当其中一个任务失败时,我在乎吗?

  • "我在乎,一个失败全盘皆输" → Promise.all
  • "我不在乎成败,我只要最快的" → Promise.race
  • "我不在乎失败,只要有一个成功就行" → Promise.any
  • "我不在乎失败,但我要知道每个的结果" → Promise.allSettled

这就是全部了。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容