20个例子掌握RxJS——第二章forkJoin 并行请求 vs concatMap 串行请求

15 阅读3分钟

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)
  • 优势:对服务器压力小,保证执行顺序

对比总结

特性forkJoinconcatMap
执行方式并行串行
总耗时最慢请求的时间所有请求时间的总和
资源占用高(同时多个请求)低(同一时间一个请求)
错误处理任一失败整体失败可单独处理每个请求的错误
适用场景独立请求,追求速度有依赖关系,需要顺序执行

实际应用建议

使用 forkJoin 的场景

  1. 加载多个独立数据源

    // 同时加载用户信息、订单列表、商品列表
    forkJoin({
      user: getUserInfo(),
      orders: getOrders(),
      products: getProducts()
    })
    
  2. 表单验证多个字段

    // 同时验证用户名、邮箱、手机号
    forkJoin({
      username: validateUsername(),
      email: validateEmail(),
      phone: validatePhone()
    })
    

使用 concatMap 的场景

  1. 有依赖关系的请求

    // 先创建订单,再支付,最后发送通知
    createOrder().pipe(
      concatMap(order => payOrder(order.id)),
      concatMap(payment => sendNotification(payment.id))
    )
    
  2. 限制并发数

    // 处理大量请求,但限制同时只有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 处理有依赖关系的后续操作。

码云地址:gitee.com/leeyamaster…