咱们来聊聊 Promise.all 和 Promise.race:并发控制的冰与火之歌 🔥❄️
各位听众,各位码农,各位未来的架构师们,晚上好!我是你们的老朋友,江湖人称“代码段子手”的AI。今天咱们不聊诗词歌赋,不谈人生哲学,咱们来聊聊 JavaScript 中两个非常重要的 API:Promise.all
和 Promise.race
。
这两个家伙,一个像团结一致的攻城部队,一个像争分夺秒的赛车手,都是并发控制的好手。但它们性格迥异,用法也大相径庭。如果用武侠小说来形容,Promise.all
就像少林寺的十八铜人阵,必须全部击破才能过关;而 Promise.race
则像是华山论剑,谁先出手谁就占得先机。
准备好了吗?系好安全带,咱们要开车啦!🚗
一、Promise.all:团结就是力量,一个都不能少!💪
1.1 初识 Promise.all:并肩作战,共创辉煌
想象一下,你要做一个复杂的网页,需要从三个不同的 API 获取数据:用户资料、商品列表、订单信息。如果一个一个地请求,那用户可要等到花儿都谢了。这时候,Promise.all
就派上用场了!
Promise.all
接受一个 Promise 数组(或者任何可迭代的 Promise),它会并发地执行这些 Promise,并返回一个新的 Promise。这个新的 Promise 只有在 所有 输入的 Promise 都成功 fulfilled 时才会 fulfilled,并且它的 fulfilled 值是一个包含所有 Promise fulfilled 值的数组,顺序与输入的 Promise 数组一致。
如果其中任何一个 Promise rejected,Promise.all
返回的 Promise 也会立即 rejected,并且以第一个 rejected 的 Promise 的 reason 作为自己的 reason。
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 100));
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // [1, 2, 3]
})
.catch((error) => {
console.error(error); // 不会执行到这里,因为所有 Promise 都成功了
});
在这个例子中,promise1
和 promise3
立即 resolve,promise2
在 100 毫秒后 resolve。Promise.all
会等待所有 Promise 完成,然后返回一个包含所有值的数组 [1, 2, 3]
。
1.2 错误处理:一荣俱荣,一损俱损
Promise.all
的错误处理机制非常严格,可谓“一荣俱荣,一损俱损”。只要有一个 Promise rejected,整个 Promise.all
就宣告失败。
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('Error!'), 50));
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // 不会执行到这里,因为 promise2 rejected
})
.catch((error) => {
console.error(error); // Error!
});
在这个例子中,promise2
在 50 毫秒后 rejected,Promise.all
立刻 rejected,并抛出 promise2
的错误信息 "Error!"。
划重点: Promise.all
的错误处理是短路的,也就是说,一旦遇到 rejected 的 Promise,它就不会等待其他 Promise 完成,而是立即 rejected。
1.3 应用场景:全都要,才能更好!
Promise.all
在以下场景中非常有用:
- 数据聚合: 从多个 API 获取数据,并将它们组合在一起,例如上面提到的用户资料、商品列表、订单信息。
- 资源加载: 并行加载多个资源,例如图片、脚本、样式表,提高页面加载速度。
- 表单验证: 并行验证多个表单字段,提高验证效率。
- 批量操作: 并行执行多个数据库操作,提高数据库操作效率。
总之,只要你需要等待多个 Promise 都完成才能继续执行,Promise.all
都是你的好帮手。
1.4 Promise.all 的局限性:成也萧何,败也萧何
Promise.all
的优点是并发执行,提高了效率。但它的缺点也很明显:
- 容错性差: 只要有一个 Promise rejected,整个
Promise.all
就失败了。这在某些情况下可能不是我们想要的,例如,我们希望尽可能地获取数据,即使某些 API 调用失败了。 - 无法追踪进度:
Promise.all
只能告诉我们所有 Promise 都完成了,但无法告诉我们每个 Promise 的进度。
针对这些局限性,我们可以使用其他方法来解决,例如:
Promise.allSettled
: 无论 Promise fulfilled 还是 rejected,都会返回结果。- 使用
map
和Promise.resolve
包装每个 Promise,并在catch
中处理错误,最后使用Promise.all
。 这样可以确保即使某些 Promise rejected,Promise.all
也能成功 fulfilled,并返回所有 Promise 的结果(包括错误信息)。
二、Promise.race:速度与激情,一马当先! 🐎
2.1 初识 Promise.race:先到先得,胜者为王
Promise.race
就像一场赛跑,谁先到达终点,谁就赢了。它接受一个 Promise 数组(或者任何可迭代的 Promise),并返回一个新的 Promise。这个新的 Promise 会采用 第一个 settled(fulfilled 或 rejected)的 Promise 的状态和值/reason。
也就是说,只要有一个 Promise fulfilled 或 rejected,Promise.race
返回的 Promise 就会立即 settled,而不管其他 Promise 的状态。
const promise1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 500));
const promise2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 100));
const promise3 = Promise.resolve(3);
Promise.race([promise1, promise2, promise3])
.then((value) => {
console.log(value); // 3
})
.catch((error) => {
console.error(error); // 不会执行到这里,因为 promise3 最先 resolve
});
在这个例子中,promise3
立即 resolve,所以 Promise.race
立刻 fulfilled,并返回 promise3
的值 3。
2.2 错误处理:成王败寇,概不负责
Promise.race
的错误处理也很简单粗暴。如果第一个 settled 的 Promise 是 rejected,那么 Promise.race
返回的 Promise 也会立即 rejected,并以该 Promise 的 reason 作为自己的 reason。
const promise1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 500));
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('Error!'), 100));
const promise3 = Promise.resolve(3);
Promise.race([promise1, promise2, promise3])
.then((value) => {
console.log(value); // 不会执行到这里,因为 promise2 最先 reject
})
.catch((error) => {
console.error(error); // Error!
});
在这个例子中,promise2
在 100 毫秒后 rejected,所以 Promise.race
立刻 rejected,并抛出 promise2
的错误信息 "Error!"。
2.3 应用场景:争分夺秒,时不我待
Promise.race
在以下场景中非常有用:
- 超时控制: 设置一个超时 Promise,如果超过指定时间没有返回结果,就 reject 该 Promise,从而避免程序一直等待。
- 竞速请求: 向多个服务器发送请求,哪个服务器先返回结果就使用哪个,提高响应速度。
- 缓存策略: 先尝试从缓存中获取数据,如果缓存中没有,再从服务器获取数据,提高数据获取速度。
- 优雅降级: 尝试使用多个不同的 API,哪个 API 先返回结果就使用哪个,提高程序的健壮性。
2.4 Promise.race 的局限性:赢家通吃,其他不管
Promise.race
的优点是速度快,但它的缺点也很明显:
- 结果不确定: 结果取决于哪个 Promise 先 settled,可能不是我们想要的。
- 资源浪费: 其他 Promise 仍然会继续执行,即使
Promise.race
已经 settled,这可能造成资源浪费。
为了避免资源浪费,我们可以在 Promise.race
settled 后,取消其他 Promise 的执行。这通常需要使用 AbortController
API。
三、Promise.all 和 Promise.race 的对比:冰与火之歌
特性 | Promise.all | Promise.race |
---|---|---|
目标 | 等待所有 Promise 完成 | 返回第一个 settled 的 Promise |
成功条件 | 所有 Promise 都 fulfilled | 任何一个 Promise fulfilled 或 rejected |
失败条件 | 任何一个 Promise rejected | 第一个 settled 的 Promise rejected |
返回值 | 包含所有 fulfilled 值的数组 | 第一个 settled 的 Promise 的值/reason |
应用场景 | 数据聚合、资源加载、表单验证、批量操作 | 超时控制、竞速请求、缓存策略、优雅降级 |
容错性 | 差 | 好 |
资源消耗 | 高 | 低(但也可能浪费资源,需要手动取消其他 Promise) |
比喻 | 少林寺十八铜人阵 | 华山论剑 |
性格 | 团结一致、严谨 | 争分夺秒、果断 |
适合人群 | 追求完美、不容有失的处女座程序员 | 追求效率、灵活应变的射手座程序员 |
表情 | 🧐 | 🚀 |
四、进阶技巧:玩转 Promise.all 和 Promise.race
4.1 使用 Promise.allSettled 增强容错性
如果我们需要尽可能地获取数据,即使某些 API 调用失败了,可以使用 Promise.allSettled
。
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve, reject) => setTimeout(() => reject('Error!'), 50));
const promise3 = Promise.resolve(3);
Promise.allSettled([promise1, promise2, promise3])
.then((results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 'Error!' },
// { status: 'fulfilled', value: 3 }
// ]
});
Promise.allSettled
返回一个数组,每个元素都是一个对象,包含 status
和 value/reason
属性。status
可以是 "fulfilled" 或 "rejected",value
是 fulfilled 的值,reason
是 rejected 的原因。
4.2 使用 AbortController 取消 Promise
如果我们需要在使用 Promise.race
后取消其他 Promise 的执行,可以使用 AbortController
API。
const controller = new AbortController();
const signal = controller.signal;
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
if (signal.aborted) {
console.log('promise1 aborted');
return;
}
resolve(1);
}, 500);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
if (signal.aborted) {
console.log('promise2 aborted');
return;
}
resolve(2);
}, 100);
});
Promise.race([promise1, promise2])
.then((value) => {
console.log(value); // 2
controller.abort(); // 取消其他 Promise
})
.catch((error) => {
console.error(error);
});
在这个例子中,promise2
比 promise1
更快 resolve,所以 Promise.race
fulfilled,并返回 promise2
的值 2。然后,我们调用 controller.abort()
取消 promise1
的执行。
4.3 组合使用 Promise.all 和 Promise.race
我们可以将 Promise.all
和 Promise.race
组合使用,以实现更复杂的并发控制逻辑。例如,我们可以使用 Promise.race
设置超时,然后在超时时间内使用 Promise.all
并行执行多个任务。
const timeout = (ms) => new Promise((resolve, reject) => setTimeout(() => reject('Timeout!'), ms));
const promise1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 200));
const promise2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 300));
Promise.race([Promise.all([promise1, promise2]), timeout(100)])
.then((value) => {
console.log(value); // Timeout!
})
.catch((error) => {
console.error(error); // Timeout!
});
在这个例子中,我们使用 Promise.race
设置了 100 毫秒的超时时间。由于 Promise.all([promise1, promise2])
需要 300 毫秒才能完成,所以 timeout(100)
先 reject,Promise.race
也随之 reject。
五、总结:掌握并发控制,成为时间的朋友
Promise.all
和 Promise.race
是 JavaScript 中非常强大的并发控制工具。它们各有优缺点,适用于不同的场景。
Promise.all
: 适合需要等待所有 Promise 都完成的场景,例如数据聚合、资源加载、表单验证、批量操作。Promise.race
: 适合需要快速响应的场景,例如超时控制、竞速请求、缓存策略、优雅降级。
通过学习和掌握 Promise.all
和 Promise.race
,我们可以编写更高效、更健壮的 JavaScript 代码,成为时间的朋友,而不是被时间追赶。
好了,今天的分享就到这里。希望大家有所收获!如果大家有什么问题,欢迎随时提问。
谢谢大家!🙏