You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NGRX Effect中switchMap与exhaustMap/concatMap的差异及异常问题

问题:NGRX Effect中switchMap替换为exhaustMap/concatMap后不再响应后续动作

我编写了如下NGRX Effect:

public SetProperTab$ = createEffect(
  () => {
    return this.actions$.pipe(
      ofType(actions.SetProperTab),
      switchMap((action) =>
        this.store$.select(selectors.GetHasLogicalProductInStore, { id: action.logicalProductId }).pipe(
          switchMap((hasLogicalProductInStore) => {
            if (hasLogicalProductInStore) {
              return of(true);
            } else {
              return this.actions$.pipe(ofType(actions.GetLogicalProductDetailsSuccess), take(1));
            }
          }),
          concatLatestFrom(() =>
            this.store$.select(selectors.GetProperSpecifiedLogicalProduct, { logicalProductId: action.logicalProductId }),
          ),
          concatMap(([_, properCurrent]) => {
            if (properCurrent?.ProductsLite.length) {
              const lastProductConfig: RestProductLiteEntity | undefined = properCurrent.ProductsLite.at(-1);
              if (lastProductConfig) {
                this.router.navigate([`/main/product-editor/${action.logicalProductId}/product-config/${lastProductConfig.ID}`]);
              }
            } else {
              this.router.navigate([`/main/product-editor/${action.logicalProductId}/global`]);
            }
            return EMPTY;
          }),
        ),
      ),
    );
  },
  { dispatch: false },
);

该Effect的逻辑是:用户点击kendo-grid列表项后触发,先检查store中是否存在该条目(及其大数据集);若不存在,则等待GetLogicalProductDetailsSuccess动作完成(该动作在后端数据下载完成后触发),之后执行对应的导航逻辑,目前使用switchMap时功能正常。

但我无法理解的是:将第一个switchMap替换为exhaustMap或concatMap后,该Effect仅在首次点击列表项时触发,后续用户点击列表项时,尽管SetProperTab动作持续分发(如下代码所示),但Effect不再响应。

public onSelectedChanged(value: ID | null): void {
    if (value) {
      this.productEditorService.onSelectedChanged(value);
      this.store$.dispatch(SetProperTab({ logicalProductId: value.toString() }));
    }
}

我原本预期它的表现与switchMap一致,因为每次点击仅分发一次SetProperTab动作,请问这是为什么?


原因分析与解释

核心问题:内部Observable未完成导致阻塞

exhaustMap和concatMap的核心特性是必须等待当前内部Observable完全完成后,才会处理下一个外部的SetProperTab动作,而你的Effect内部存在Observable无法及时完成的场景:

  • 当store中已有目标数据时,返回的of(true)会立即完成,不会有问题;
  • 当store中无目标数据时,你返回的是this.actions$.pipe(ofType(actions.GetLogicalProductDetailsSuccess), take(1))——这个Observable只有在收到GetLogicalProductDetailsSuccess动作时才会完成。如果这个动作迟迟未触发(比如后端请求超时、或者该动作是针对其他ID的),这个内部Observable会一直处于活跃状态,导致exhaustMap直接忽略后续的SetProperTab动作,concatMap则把后续动作积压在队列里无法执行。

而switchMap的特性是只要新的SetProperTab动作到来,就会立即取消当前正在执行的内部Observable,转而处理新动作,所以不会出现被阻塞的情况,这就是两者表现差异的根源。

为什么“单次点击分发动作”仍有差异?

你关注的是外部动作的分发次数,但exhaustMap/concatMap的判断依据是内部Observable是否完成,而不是外部动作的数量。只要第一个内部Observable还在等待(未收到对应的GetLogicalProductDetailsSuccess),后续的SetProperTab动作要么被忽略,要么被排队,直到前面的内部Observable完成才会被处理。

快速验证方法

可以在内部Observable的返回语句后添加tap操作,打印完成日志:

// 已有数据时
return of(true).pipe(tap(() => console.log(`内部Observable完成:${action.logicalProductId}`)));
// 无数据时
return this.actions$.pipe(
  ofType(actions.GetLogicalProductDetailsSuccess),
  take(1),
  tap(() => console.log(`内部Observable完成:${action.logicalProductId}`))
);

如果某些操作后没有打印日志,说明对应的内部Observable未完成,直接导致了exhaustMap/concatMap的阻塞。


内容的提问来源于stack exchange,提问作者Bartek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 15:33:31