详细讲解在 JavaScript 中如何利用 Promise.all 与 Promise.allSettled 并行发起多个 HTTP 请求并等待结果,包含双阶段解析、错误熔断与容错降级、超时控制及并发池限流机制。
JavaScript中怎么并行发送多个HTTP请求并等待结果
时间:2026-11-03
浏览:166
文章目录
Promise.all 或 Promise.allSettled 方法,配合原生 fetch 与 Array.prototype.map。具体做法是将需要请求的 URL 数组映射为一组立即执行并处于 Pending 状态的 Promise,随后使用 await Promise.all(promises) 同步等待所有请求返回并解析数据。对于需要容忍部分接口报错的场景,改用 Promise.allSettled 即可获取每个请求的具体状态;若涉及数十甚至上百个并发连接,则应通过并发池限制同时处于连接状态的数量,防止击垮服务或触发布局限流。
一、为什么需要并行请求:对比顺序阻塞执行
在处理数据可视化看板、聚合首页或批量文件上传等场景时,页面往往需要同时请求多个不同微服务或端点的数据。初学者容易误在数组循环中直接使用 await,例如采用 for...of 循环逐个请求,这种写法会将网络请求退化为串行执行。
在串行模式下,后一个请求必须等待前一个请求完成 TCP 握手、服务端处理并完整传输响应体后才能启动,所有请求的等待时间呈线性累加。而现代浏览器与 Node.js 具备高效的异步 I/O 能力,借助 Promise 可以让所有 HTTP 请求在同一时刻全部触发,浏览器底层的网络线程池会并行调度并发送它们。整个操作的完成耗时不再是各请求耗时的总和,而是取决于耗时最长的那个长尾请求。
二、标准实现:使用 Promise.all 并行发起与数据解析
在浏览器原生环境或现代化 Node.js(v18+)中,fetch 是发起 HTTP 请求的标准 API。使用 Promise.all 并发处理多个请求时,需要特别注意 fetch 的“双阶段异步特性”:
- 网络响应阶段:
fetch(url)返回的 Promise 仅代表 HTTP 响应头已经到达(此时解析出Response对象)。 - 数据体读取阶段:调用
response.json()或response.text()是另一个异步过程,需要再次等待流式数据读取完毕。
因此,最佳实践是在 map 映射回调中封装一个微型异步任务,将“发起网络请求”与“解析响应 JSON”打包为一个统一的 Promise,再将 Promise 数组交付给 Promise.all。
完整代码示例:Promise.all 并行请求
// 1. 目标接口列表
const apiEndpoints = [
'https://api.example.com/user/profile',
'https://api.example.com/user/posts',
'https://api.example.com/user/followers'
];
// 2. 并行请求并统一等待的执行函数
async function fetchDashboardData(urls) {
try {
// map 遍历时会立刻同步调用 fetch,所有请求同时发出,返回 Promise 数组
const requestTasks = urls.map(async (url) => {
const response = await fetch(url);
// 注意:fetch 在遭遇 404 或 500 时不会自动 reject,必须手动校验 ok 状态
if (!response.ok) {
throw new Error(`请求失败: ${url} 状态码: ${response.status}`);
}
return await response.json();
});
// 3. 并行等待所有 Promise 完成解析
const [profile, posts, followers] = await Promise.all(requestTasks);
// 4. 消费已解构的业务数据
console.log('个人资料:', profile);
console.log('文章列表:', posts);
console.log('粉丝列表:', followers);
return { profile, posts, followers };
} catch (error) {
// 只要有任一接口报错,Promise.all 就会立即触发 catch
console.error('聚合请求发生错误:', error.message);
throw error;
}
}
Promise.all 返回的结果数组,其元素顺序与传入的任务数组顺序完全一致,绝不会因为哪个请求先返回而被打乱。因此开发者可以放心地通过数组解构(如 const [a, b, c] = results)按序取值。
三、容错与降级:Promise.all 与 Promise.allSettled 的选择
Promise.all 具备“全有或全无(Fail-Fast)”的特点。在实际生产业务中,并非所有接口都是强依赖的。如果页面的核心数据(如用户信息)已经成功获取,而附带的边缘数据(如推荐广告或天气微件)由于网络抖动发生 500 报错,使用 Promise.all 会导致整个页面直接崩溃进入错误分支。
为了解决局部降级问题,ECMAScript 2020 引入了 Promise.allSettled。它会始终等待全部请求履约完毕,无论每个请求是成功(fulfilled)还是失败(rejected),外部的 Promise 都会正常 resolve,并返回一个包含每个任务独立状态的对象数组。
使用 Promise.allSettled 实现模块级容错
async function fetchSafeModules(tasks) {
// 批量发出请求
const promises = tasks.map(async (task) => {
const res = await fetch(task.url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return { id: task.id, data: await res.json() };
});
// 使用 allSettled,永远不会直接抛出异常被 catch
const settleResults = await Promise.allSettled(promises);
// 遍历分流处理成功与失败项
settleResults.forEach((item, index) => {
if (item.status === 'fulfilled') {
console.log(`模块 ${tasks[index].id} 加载成功:`, item.value.data);
renderComponent(tasks[index].id, item.value.data);
} else {
console.warn(`模块 ${tasks[index].id} 降级展示:`, item.reason.message);
renderFallbackPlaceholder(tasks[index].id);
}
});
}
四、进阶场景:限制最大并发数的“并发池”机制
虽然并行请求可以最大化降低耗时,但并行数量绝不是越多越好。在浏览器环境中,针对同一个域名的 HTTP/1.1 连接存在通常为 6 个的最大并发连接数限制;而在 HTTP/2 环境下,如果一次性发起上百个请求,也极易导致客户端 CPU 瞬时占用过高,或者触发服务端网关的限流规则(HTTP 429 Too Many Requests)。
当需要处理数十甚至上百个并发请求(如批量导出、大批量图片预加载、数据切片同步)时,标准的方案是引入并发池(Concurrency Pool),设定一个并发上限阈值(如每次最多运行 4 到 6 个请求),当有请求完成时立即从队列中填入新请求。
手写原生轻量级并发池(无需外部依赖)
/**
* 原生并发池控制函数
* @param {Array} poolList 待请求的数据项列表
* @param {number} limit 最大并发限制数量
* @param {Function} iteratorFn 处理单个项的异步函数
*/
async function asyncPool(poolList, limit, iteratorFn) {
const allResults = [];
const executingTasks = new Set();
for (const item of poolList) {
// 包装单个异步执行 Promise
const task = Promise.resolve().then(() => iteratorFn(item));
allResults.push(task);
executingTasks.add(task);
// 请求完成后将其从正在执行的 Set 集合中移出
const cleanUp = () => executingTasks.delete(task);
task.then(cleanUp, cleanUp);
// 如果当前并发数达到了设定的上限,使用 Promise.race 等待其中任意一个槽位空出
if (executingTasks.size >= limit) {
await Promise.race(executingTasks);
}
}
// 等待最后剩余在池中的所有任务完成并按原始顺序返回
return Promise.all(allResults);
}
// 使用示例:批量下载 100 个资源,并发数严格限制为 5
const fileIds = Array.from({ length: 100 }, (_, i) => i + 1);
asyncPool(fileIds, 5, async (id) => {
const res = await fetch(`/api/files/${id}`);
return await res.json();
}).then(results => {
console.log('全部 100 个文件获取完毕,数量:', results.length);
});
五、生产级避坑指南与最佳实践
在真实项目的并行请求开发中,除了选用正确的 Promise API 之外,还需要规避以下关键陷阱:
1. 必须配置 AbortController 防止长尾请求死锁
Promise.all 的整体执行时间完全受制于“最慢的那一个请求”。如果其中某个接口由于后端死锁或网络丢包陷入无限挂起,会导致整个聚合操作一直处于 Pending 状态。生产环境应为并行请求配置超时取消机制:
function fetchWithTimeout(url, timeoutMs = 8000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
return fetch(url, { signal: controller.signal })
.then(res => {
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.catch(err => {
clearTimeout(timer);
if (err.name === 'AbortError') {
throw new Error(`请求超时: ${url}`);
}
throw err;
});
}
2. 并行请求方案选型对照表
| 机制方案 | 失败响应行为 | 返回值结构 | 核心适用场景 |
|---|---|---|---|
| Promise.all | 立即 Reject 第一个错误(快速失败) | 数据值数组 [A, B, C] |
彼此强依赖的数据源、原子性操作、基础依赖加载 |
| Promise.allSettled | 等待全部结束,永远不直接 Reject | 状态对象数组 [{status, value/reason}] |
组件独立渲染、报表批量统计、多源异构数据采集与降级 |
| 并发池 (asyncPool) | 结合 Promise.all/allSettled 进行错误处理 | 按原输入顺序返回的聚合结果 | 超过 10+ 的海量批量请求、网络爬虫、文件切片分块上传 |
urls.map() 结合 Promise.all 发起;对于允许部分组件降级的看板页面,果断选用 Promise.allSettled;而在批量处理大量接口数据时,务必加上并发池限流,兼顾吞吐效率与系统稳定性。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。