RxJS 实战:forkJoin 并行请求 vs concatMap 串行请求
概述
在处理多个异步请求时,我们有两种主要策略:并行执行和串行执行。本章将对比 RxJS 中的 forkJoin(并行)和 concatMap(串行)两种方式,帮助你在不同场景下做出正确的选择。
forkJoin:并行执行
forkJoin 会同时发起所有请求,等待所有请求完成后,将所有结果一起返回。
特点
- 并发执行:所有请求同时发起
- 等待全部完成:必须等待所有请求都完成后才返回结果
- 结果顺序:结果数组的顺序与输入 Observable 的顺序一致
- 错误处理:如果任何一个请求失败,整个
forkJoin会失败
适用场景
- 多个独立的请求,可以并行执行
- 需要等待所有请求完成后才能进行下一步操作
- 对响应时间要求较高,希望尽快得到所有结果
代码示例
const delay1$ = this.http.get<DelayApiResponse>(`${this.apiBaseUrl}/api/delay1`);
const delay2$ = this.http.get<DelayApiResponse>(`${this.apiBaseUrl}/api/delay2`);
const delay3$ = this.http.get<DelayApiResponse>(`${this.apiBaseUrl}/api/delay3`);
forkJoin({
delay1: delay1$,
delay2: delay2$,
delay3: delay3$
})
.subscribe({
next: (results) => {
// 所有请求成功,结果按对象键名组织
this.delay1Result = results.delay1;
this.delay2Result = results.delay2;
this.delay3Result = results.delay3;
// 总耗时约为最慢接口的时间(约3秒)
},
error: (err) => {
console.error('请求失败:', err);
}
});
性能分析
假设三个接口的延迟时间分别是 1 秒、2 秒、3 秒:
- 总耗时:约 3 秒(最慢接口的时间)
- 优势:速度快,充分利用并发能力
concatMap:串行执行
concatMap 会按顺序执行请求,前一个请求完成后才开始下一个请求。
特点
- 顺序执行:请求按顺序一个接一个执行
- 保证顺序:严格按照输入顺序执行
- 错误处理:单个请求失败不会中断整个流程(如果使用
catchError) - 资源占用:同一时间只有一个请求在进行
适用场景
- 请求之间有依赖关系,必须按顺序执行
- 需要限制并发数,避免服务器压力过大
- 需要保证请求的执行顺序
代码示例
from([delay1$, delay2$, delay3$])
.pipe(
concatMap((req$, idx) =>
req$.pipe(
// 捕获单个请求错误,包裹返回
catchError(err => of({
success: false,
message: err.message || '请求失败',
data: { delay: null, timestamp: null, info: '请求失败' }
}))
)
),
toArray() // 收集所有结果
)
.subscribe({
next: (results: any[]) => {
// results 顺序: [delay1, delay2, delay3]
this.delay1Result = results[0];
this.delay2Result = results[1];
this.delay3Result = results[2];
// 总耗时约为所有接口时间的总和(约6秒)
}
});
性能分析
假设三个接口的延迟时间分别是 1 秒、2 秒、3 秒:
- 总耗时:约 6 秒(1 + 2 + 3)
- 优势:对服务器压力小,保证执行顺序
对比总结
| 特性 | forkJoin | concatMap |
|---|---|---|
| 执行方式 | 并行 | 串行 |
| 总耗时 | 最慢请求的时间 | 所有请求时间的总和 |
| 资源占用 | 高(同时多个请求) | 低(同一时间一个请求) |
| 错误处理 | 任一失败整体失败 | 可单独处理每个请求的错误 |
| 适用场景 | 独立请求,追求速度 | 有依赖关系,需要顺序执行 |
实际应用建议
使用 forkJoin 的场景
-
加载多个独立数据源
// 同时加载用户信息、订单列表、商品列表 forkJoin({ user: getUserInfo(), orders: getOrders(), products: getProducts() }) -
表单验证多个字段
// 同时验证用户名、邮箱、手机号 forkJoin({ username: validateUsername(), email: validateEmail(), phone: validatePhone() })
使用 concatMap 的场景
-
有依赖关系的请求
// 先创建订单,再支付,最后发送通知 createOrder().pipe( concatMap(order => payOrder(order.id)), concatMap(payment => sendNotification(payment.id)) ) -
限制并发数
// 处理大量请求,但限制同时只有3个 from(requests).pipe( mergeMap(req => req, 3) // 并发数限制为3 )
错误处理策略
forkJoin 的错误处理
forkJoin({
delay1: delay1$.pipe(catchError(err => of(null))),
delay2: delay2$.pipe(catchError(err => of(null))),
delay3: delay3$.pipe(catchError(err => of(null)))
})
concatMap 的错误处理
from([delay1$, delay2$, delay3$]).pipe(
concatMap(req$ =>
req$.pipe(
catchError(err => of({ error: err.message }))
)
)
)
总结
选择 forkJoin 还是 concatMap,主要取决于你的具体需求:
- 追求速度,请求独立 → 使用
forkJoin - 有依赖关系,需要顺序 → 使用
concatMap - 需要限制并发 → 使用
mergeMap配合并发数参数
在实际项目中,根据业务场景灵活选择,有时候也可以组合使用,比如先用 forkJoin 并行加载基础数据,再用 concatMap 处理有依赖关系的后续操作。