各位观众老爷们,晚上好!我是今晚的主讲人,很高兴能跟大家一起聊聊 JavaScript 里两个非常有意思的家伙:Promise.all
和 Promise.race
。别看它们名字挺酷炫,其实用起来也挺简单,关键在于理解它们在并发控制中的作用。今天咱们就来好好扒一扒这两个“并发小能手”。
一、并发控制是个啥?为啥要并发控制?
要理解 Promise.all
和 Promise.race
,首先得明白“并发控制”是个啥。简单来说,并发控制就是同时处理多个任务,并且保证这些任务能够高效、稳定地执行。
想象一下,你开了个小吃摊,同时来了好几个客人,有的要肉夹馍,有的要凉皮,有的要冰峰。如果你一个一个地做,那后面的客人估计要饿死了。但如果你能同时做肉夹馍、凉皮,还能让小弟去拿冰峰,效率是不是就大大提高了?这就是并发的好处。
在 JavaScript 的世界里,并发通常指的是同时发起多个异步请求,比如从不同的服务器获取数据。如果不进行并发控制,可能会出现以下问题:
- 阻塞主线程: 异步请求还没回来,主线程就被卡住了,页面就没反应了,用户体验极差。
- 请求过多: 同时发起太多请求,服务器扛不住了,直接崩给你看。
- 资源浪费: 有些请求可能相互依赖,如果并行执行,会导致不必要的资源浪费。
所以,我们需要并发控制,来合理地管理这些异步任务,让它们既能高效执行,又不会搞垮我们的系统。
二、Promise.all
:一个都不能少!
Promise.all
的作用就像它的名字一样,它会等待所有的 Promise 对象都 resolve 或者 reject 后,才会返回结果。它接受一个 Promise 对象数组作为参数,并返回一个新的 Promise 对象。
- 如果数组中所有的 Promise 对象都 resolve 了,
Promise.all
返回的 Promise 对象也会 resolve,并且 resolve 的值是一个包含所有 Promise 对象 resolve 值的数组,数组的顺序与传入的 Promise 对象数组的顺序一致。 - 如果数组中任何一个 Promise 对象 reject 了,
Promise.all
返回的 Promise 对象也会立即 reject,并且 reject 的值是第一个被 reject 的 Promise 对象的 reject 值。
是不是有点绕?没关系,咱们上代码:
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.random();
if (random > 0.2) {
resolve(`Data from ${url}: ${random}`);
} else {
reject(`Failed to fetch data from ${url}: ${random}`);
}
}, Math.random() * 1000); // 模拟网络延迟
});
}
const urls = ['/api/data1', '/api/data2', '/api/data3'];
Promise.all(urls.map(url => fetchData(url)))
.then(results => {
console.log('All data fetched successfully:', results);
})
.catch(error => {
console.error('Failed to fetch data:', error);
});
在这个例子中,fetchData
函数模拟了一个异步请求,它会随机 resolve 或者 reject。我们使用 Promise.all
来同时请求三个不同的 API 接口。
- 如果三个请求都成功了, 控制台会输出类似:
All data fetched successfully: [ 'Data from /api/data1: 0.8...', 'Data from /api/data2: 0.9...', 'Data from /api/data3: 0.7...' ]
- 如果其中一个请求失败了, 控制台会输出类似:
Failed to fetch data: Failed to fetch data from /api/data2: 0.1...
Promise.all
的一个典型应用场景是:你需要同时获取多个数据源的数据,并且只有当所有数据都获取成功后,才能进行下一步操作。比如,你需要同时从用户资料、用户订单、用户权限三个接口获取数据,才能渲染用户个人中心页面。
Promise.all
的优点:
- 并行执行: 提高效率,缩短整体耗时。
- 统一处理: 可以方便地处理所有 Promise 对象都成功或失败的情况。
Promise.all
的缺点:
- 容错性较差: 只要有一个 Promise 对象 reject 了,整个
Promise.all
就会立即 reject,即使其他 Promise 对象已经 resolve 了。这可能会导致一些不必要的失败。
三、Promise.race
:跑得最快的就是赢家!
Promise.race
的作用就像赛跑一样,它会等待第一个 Promise 对象 resolve 或者 reject 后,就立即返回结果。它也接受一个 Promise 对象数组作为参数,并返回一个新的 Promise 对象。
- 如果数组中第一个 resolve 的 Promise 对象 resolve 了,
Promise.race
返回的 Promise 对象也会 resolve,并且 resolve 的值是第一个 resolve 的 Promise 对象的 resolve 值。 - 如果数组中第一个 reject 的 Promise 对象 reject 了,
Promise.race
返回的 Promise 对象也会 reject,并且 reject 的值是第一个 reject 的 Promise 对象的 reject 值。
继续上代码:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(`Timeout after ${ms} ms`);
}, ms);
});
}
const apiRequest = fetchData('/api/data'); // 假设 fetchData 是一个网络请求函数
Promise.race([apiRequest, timeout(500)])
.then(result => {
console.log('API request succeeded:', result);
})
.catch(error => {
console.error('API request failed:', error);
});
在这个例子中,我们使用 Promise.race
来给 API 请求设置一个超时时间。
- 如果 API 请求在 500 毫秒内成功返回, 控制台会输出类似:
API request succeeded: Data from /api/data: 0.8...
- 如果 API 请求超过 500 毫秒还没有返回,
timeout
函数会 reject,Promise.race
也会 reject,控制台会输出:API request failed: Timeout after 500 ms
Promise.race
的一个典型应用场景是:你需要给某个操作设置一个超时时间,或者你需要从多个数据源获取数据,只需要获取第一个返回的结果即可。比如,你需要同时从 CDN 和服务器获取图片,只需要获取最先返回的图片即可。
Promise.race
的优点:
- 快速响应: 可以快速地获取到第一个返回的结果,提高用户体验。
- 超时控制: 可以给耗时操作设置超时时间,避免长时间等待。
Promise.race
的缺点:
- 只能获取第一个结果: 无法获取到其他 Promise 对象的结果。
- 可能错过最优结果: 第一个返回的结果不一定是最好的结果。
四、Promise.all
和 Promise.race
的对比
为了更清晰地理解 Promise.all
和 Promise.race
的区别,我们用表格来做一个对比:
特性 | Promise.all |
Promise.race |
---|---|---|
作用 | 等待所有 Promise 对象都 resolve 或 reject 后返回结果 | 等待第一个 Promise 对象 resolve 或 reject 后返回结果 |
返回值 | 包含所有 Promise 对象 resolve 值的数组,或第一个 reject 值 | 第一个 resolve 的 Promise 对象的 resolve 值,或第一个 reject 值 |
执行方式 | 并行执行 | 并行执行 |
容错性 | 较差,只要有一个 reject 就立即 reject | 较好,只关心第一个返回的结果 |
应用场景 | 需要所有 Promise 对象都成功才能进行下一步操作 | 只需要获取第一个返回的结果,或需要设置超时时间 |
五、并发控制的进阶技巧
Promise.all
和 Promise.race
只是并发控制的基础工具,在实际开发中,我们可能需要更复杂的并发控制策略。
- 限制并发数量: 如果同时发起太多请求,可能会导致服务器压力过大。我们可以使用一些第三方库,比如
p-limit
,来限制并发数量。
const pLimit = require('p-limit');
const limit = pLimit(5); // 限制并发数量为 5
const urls = Array.from({ length: 20 }, (_, i) => `/api/data${i + 1}`);
const promises = urls.map(url => limit(() => fetchData(url))); // 使用 limit 包裹 fetchData
Promise.all(promises)
.then(results => {
console.log('All data fetched successfully:', results);
})
.catch(error => {
console.error('Failed to fetch data:', error);
});
在这个例子中,我们使用 p-limit
限制了并发数量为 5,这意味着最多同时只有 5 个 fetchData
函数在执行。
- 使用 Async/Await: Async/Await 可以让我们用同步的方式编写异步代码,让代码更易读、易维护。
async function fetchDataSequentially() {
for (const url of urls) {
try {
const data = await fetchData(url);
console.log(`Data from ${url}:`, data);
} catch (error) {
console.error(`Failed to fetch data from ${url}:`, error);
}
}
}
fetchDataSequentially();
在这个例子中,我们使用 Async/Await 顺序地请求每个 API 接口,只有当前请求完成后,才会发起下一个请求。
- 结合使用: 我们可以将
Promise.all
和Promise.race
结合起来使用,来构建更复杂的并发控制策略。
例如,我们可以使用 Promise.race
来给每个请求设置一个超时时间,如果请求超时了,就放弃这个请求,并尝试从其他数据源获取数据。
六、总结
Promise.all
和 Promise.race
是 JavaScript 中非常有用的并发控制工具。Promise.all
适用于需要等待所有 Promise 对象都完成的场景,而 Promise.race
适用于只需要获取第一个返回结果的场景。
当然,并发控制不仅仅是 Promise.all
和 Promise.race
这么简单,它涉及到很多复杂的策略和技巧。我们需要根据实际情况,选择合适的并发控制方案,才能保证我们的系统能够高效、稳定地运行。
希望今天的讲座能帮助大家更好地理解 Promise.all
和 Promise.race
,并在实际开发中灵活运用它们。
感谢各位的观看!下次再见!