RxJS 实战:使用 mergeMap 实现并发请求
概述
在实际开发中,我们经常需要并发执行多个请求。虽然 forkJoin 也可以实现并发,但它要求等待所有请求完成。而 mergeMap 提供了更灵活的并发控制,可以实时处理每个请求的结果。本章将详细介绍如何使用 mergeMap 实现并发请求。
mergeMap 操作符简介
mergeMap(也称为 flatMap)是 RxJS 中最常用的操作符之一。它会将源 Observable 的每个值映射为一个新的 Observable,然后合并这些 Observable 的输出。
基本语法
source$.pipe(
mergeMap(value => createObservable(value))
)
关键特性
- 并发执行:默认情况下,
mergeMap会并发执行所有内部 Observable - 实时输出:每个内部 Observable 完成后立即输出结果,不等待其他 Observable
- 顺序不保证:结果的顺序可能与输入顺序不同(按完成时间排序)
- 并发控制:可以通过第二个参数限制并发数
实战场景:并发请求多个接口
假设我们需要并发请求 10 个接口(delay1、delay2、delay3 循环),并实时显示每个请求的结果。
实现思路
- 使用
from将接口 URL 数组转换为 Observable - 使用
mergeMap并发执行每个请求 - 使用
catchError处理单个请求的错误 - 使用
toArray收集所有结果(可选,如果需要等待全部完成)
核心代码
const apiUrls = [
'/api/delay1',
'/api/delay2',
'/api/delay3',
'/api/delay1',
'/api/delay2',
'/api/delay3',
'/api/delay1',
'/api/delay2',
'/api/delay3',
'/api/delay1'
];
from(apiUrls)
.pipe(
mergeMap((url, index) => {
const apiName = url.replace('/api/', '');
return this.http.get<DelayApiResponse>(`${this.apiBaseUrl}${url}`)
.pipe(
catchError(err => {
// 捕获单个请求错误,返回错误响应
console.error(`请求 ${url} 失败:`, err);
return of({
success: false,
message: err.message || '请求失败',
data: {
delay: null as any,
timestamp: new Date().toISOString(),
info: '请求失败'
}
} as DelayApiResponse);
}),
map(response => {
// 将响应包装为包含接口名称的对象
const result: RequestResult = {
apiName,
apiUrl: url,
response,
index
};
return result;
})
);
}),
toArray() // 收集所有结果
)
.subscribe({
next: (results: RequestResult[]) => {
// 所有请求完成(按完成顺序,不是原始顺序)
this.results = results;
console.log('所有请求完成:', results);
}
});
关键点解析
1. 并发执行机制
mergeMap 默认并发数为 Infinity,意味着理论上所有请求都会并发执行。但实际并发数可能受以下因素限制:
- 浏览器并发连接限制:同一域名通常最多 6 个并发连接
- HTTP 客户端限制:某些 HTTP 客户端可能有自己的并发限制
- 服务器限制:服务器可能限制同一客户端的并发连接数
2. 结果顺序
mergeMap 输出的结果顺序是按照完成时间排序的,而不是输入顺序。如果需要保持原始顺序,可以使用 concatMap 或 toArray() 后再排序。
3. 错误处理
使用 catchError 可以捕获单个请求的错误,避免整个流中断。这样即使某个请求失败,其他请求仍能继续执行。
4. 并发数控制
如果需要限制并发数,可以在 mergeMap 的第二个参数中指定:
mergeMap((url, index) => {
return this.http.get(url);
}, 3) // 最多同时 3 个请求
与 forkJoin 的对比
| 特性 | mergeMap | forkJoin |
|---|---|---|
| 执行方式 | 并发,实时输出 | 并发,等待全部完成 |
| 结果顺序 | 按完成时间 | 按输入顺序 |
| 错误处理 | 可单独处理 | 任一失败整体失败 |
| 适用场景 | 需要实时处理结果 | 需要等待全部完成 |
实际应用场景
1. 批量数据验证
// 并发验证多个用户ID是否存在
from(userIds)
.pipe(
mergeMap(userId =>
this.http.get(`/api/users/${userId}`).pipe(
catchError(() => of(null))
)
)
)
.subscribe(result => {
// 实时处理每个验证结果
if (result) {
console.log('用户存在:', result);
}
});
2. 文件上传进度
// 并发上传多个文件,实时显示进度
from(files)
.pipe(
mergeMap(file =>
this.uploadFile(file).pipe(
map(progress => ({ file, progress }))
)
)
)
.subscribe(({ file, progress }) => {
// 实时更新每个文件的上传进度
updateFileProgress(file, progress);
});
3. 数据采集
// 从多个数据源并发采集数据
from(dataSources)
.pipe(
mergeMap(source =>
this.fetchData(source).pipe(
catchError(err => {
console.error(`数据源 ${source} 失败:`, err);
return EMPTY;
})
)
)
)
.subscribe(data => {
// 实时处理采集到的数据
processData(data);
});
性能优化建议
1. 限制并发数
如果请求数量很大,建议限制并发数,避免服务器压力过大:
mergeMap(url => this.http.get(url), 5) // 最多 5 个并发
2. 使用 exhaustMap 避免重复请求
如果同一个请求可能被触发多次,可以使用 exhaustMap 忽略后续请求:
exhaustMap(url => this.http.get(url))
3. 添加重试机制
对于可能失败的请求,可以添加重试:
mergeMap(url =>
this.http.get(url).pipe(
retry(3), // 失败后重试 3 次
catchError(err => of(null))
)
)
注意事项
- 内存占用:如果请求数量非常大,需要注意内存占用
- 服务器压力:并发请求会给服务器带来压力,需要合理控制并发数
- 错误处理:确保每个请求都有适当的错误处理,避免整个流中断
- 结果顺序:如果需要保持顺序,考虑使用
concatMap或toArray()后排序
总结
mergeMap 是实现并发请求的强大工具,它提供了灵活的并发控制和实时结果处理能力。在实际项目中,根据具体需求选择合适的策略:
- 需要实时处理结果 → 使用
mergeMap - 需要等待全部完成 → 使用
forkJoin - 需要保证顺序 → 使用
concatMap - 需要限制并发 → 使用
mergeMap配合并发数参数
通过合理使用 mergeMap,我们可以构建高效、响应迅速的异步数据流处理系统。