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

JavaScript 里 Promise 有四个用于并发调度的静态方法——Promise.all、Promise.race、Promise.any和Promise.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 一次。 第一个调用 resolve 或 reject 的人赢了,后面的调用自动失效。你不需要写任何"判断是否已经完成"的逻辑——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辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容