【问题标题】:How can I use switchMap instead of nested subscriptions?如何使用 switchMap 而不是嵌套订阅?
【发布时间】:2020-12-09 15:24:08
【问题描述】:

我还在学习 observables,所以如果有一个简单的解决方案,我不会感到惊讶。基本上我现在拥有的是四个嵌套订阅,第二个 subscribe() 中有一个fourEach。我看到了很多使用 switchMap 的答案,但我找不到一个也有一个 for 循环可以迭代的答案。我知道我可能应该使用嵌套订阅,但我不知道如何使用 forEach。

这是嵌套订阅的工作代码:

dialogRef.afterClosed().subscribe(result => {
  if(result) {
    this.createLikertResponseGroup(result.likertResponseGroup)
      .subscribe(likertResponseGroupJSON => {

        result.likertResponse.controls.likertResponseFormArray.controls.forEach((element) => {
          let characteristic = element.controls.characteristic;
          this.newResponseGroupId = likertResponseGroupJSON.LikertResponseGroup.id;

          this.createLikertResponse(element, this.newResponseGroupId)
            .subscribe(likertResponseJSON => {

              if (characteristic) {
                let responseId = likertResponseJSON.LikertResponse.id;

                this.createCharacteristic(characteristic, this.newResponseGroupId, responseId)
                  .subscribe(characteristicJSON => {
                    this.newCharacteristicId = characteristicJSON.Characteristic.id;
                  });
              }
            });
        });
      })
  }
});

我现在的作品。所以我的问题是,是否值得改变我这样做的方式?如果是这样,我会怎么做?

我还没有走多远,但我对 switchMap 的尝试看起来像这样:

dialogRef.afterClosed().pipe(
  filter(result => result != null),
  switchMap(result => 
    from(result.likertResponse.controls.likertResponseFormArray.controls).pipe(
      // not sure what to do after this (or if I'm even doing it right)
    )
  ),
);

【问题讨论】:

  • 使用 switch map 的想法是,第一个“可观察部分”应该返回一个新的 observable,而不是订阅一个新的 observable。 switchMap 将在它们之间起到粘合作用,并且基本上将 subscribe 替换为“完成第一个订阅并切换到另一个”。现在,在您的第一个案例中,您有一个 if 和两个案例。所以,如果你想使用 switchmap,你必须在这两种情况下都返回一个 observable。什么都不想做的情况下,可以返回Observable.empty。在正常情况下,返回this.createLikertResponseGroup(result.likertResponseGroup)
  • 作为辅助建议,为了试验可观察对象和学习,您可以使用 StackBlitz。或者只是去https://rxjs.dev/ 并打开控制台,rxjs 已加载在那里。只需替换您的实际 API 调用 dialogRef.afterClosed() 返回一些您硬编码的虚拟 Observable(可能使用一些“setTimeout”来模拟异步性),以及许多 console.log 以可视化程序的流程。
  • 我建议你阅读这篇文章:medium.com/angular-in-depth/…

标签: angular rxjs angular-material angular2-observables


【解决方案1】:

mergeMap 而不是嵌套subscribe

mergeMap 完成嵌套订阅所做的所有事情,但它还允许您在发出订阅值时继续执行逻辑。


别急:

如果您订阅的 observable 发出一次并完成(如 http 请求),switchMapmergeMap 会产生相同的输出。在这些情况下,switchMap 通常比mergeMap 更推荐。原因从调试内存泄漏到边缘性能,再到其他开发人员的期望。

为简单起见,我在这里忽略了这一点,并在所有情况下都使用了mergeMap


您可以通过嵌套 mergeMap 和/或嵌套 subscriptions 来隐藏一些复杂性,因为您可以依靠函数闭包在管道中更早地设置和记住值。

这也可能成为造成极大混乱的原因。众所周知,深度嵌套的函数很难在 JS 中调试,因此映射到中间对象以保存下一步所需的值(而不是嵌套并通过函数闭包获取中间值)的额外努力非常值得。

它也稍微快了一点,因为运行时不需要在调用堆栈中向上移动以查找变量(但同样,您应该这样做,因为它更清洁、可维护和可扩展,而不是为了尽早优化)。

这是用 mergeMap 和包含中间值的对象完全重写的代码:

dialogRef.afterClosed().pipe(
  filter(result => result), // <-- only "truthy" results pass same as if(result)
  mergeMap(result =>
    this.createLikertResponseGroup(result.likertResponseGroup).pipe(
      map(likertResponseGroupJSON => ({result, likertResponseGroupJSON}))
    )
  ),
  mergeMap(({result, likertResponseGroupJSON}) => merge(
    ...result.likertResponse.controls.likertResponseFormArray.controls.map(
      element => this.createLikertResponse(
        element, 
        likertResponseGroupJSON.LikertResponseGroup.id
      ).pipe(
        map(likertResponseJSON => ({
          likertResponseJSON,
          characteristic: element.controls.characteristic,
          newResponseGroupId: likertResponseGroupJSON.LikertResponseGroup.id
        }))
      )
    )
  )),
  filter(({characteristic}) => characteristic) // only "Truthy" characteristic allowed
  mergeMap(({likertResponseJSON, characteristic, newResponseGroupId}) =>
    this.createCharacteristic(
      characteristic, 
      newResponseGroupId, 
      likertResponseJSON.LikertResponse.id
    ).pipe(
      map(characteristicJSON => ({
        newCharacteristicId: characteristicJSON.Characteristic.id,
        newResponseGroupId
      }))
    )
  )
).subscribe(({newCharacteristicId, newResponseGroupId}) => {
  this.newResponseGroupId = newResponseGroupId;
  this.newCharacteristicId = newCharacteristicId;
});

merge/forkJoin/concat 而不是forEach(stream.subscribe())

您会在上面的代码中注意到,当需要重写您的 forEach 循环时,我使用了 mergeArray#map 的组合,而不是 Array#forEach

merge 等同于 forEach(stream.subscribe()) 的关闭,但其他方式可以改变行为,甚至可以提高性能,或者只是让您直观地组合更复杂的流。

在这里,第 2 行和第 3 行具有相同的输出。然而,第二个很容易扩展更多的 RxJS 操作符

1. const arrayOfStreams = [s1,s2,s3,s4];
2. arrayOfStreams.forEach(s => s.subscribe(console.log));
3. merge(...arrayOfStreams).subscribe(console.log);

扩展:

arrayOfStreams.forEach(s => s.subscribe(value => {
  if(this.isGoodValue(value)){
    console.log(value.append(" end"))
  }
}));

merge(...arrayOfStreams).pipe(
  filter(this.isGoodValue),
  map(value => value.append(" end"))
).subscribe(console.log);

【讨论】:

  • switchMap 和 mergeMap 在行为上存在显着差异,将其归结为一些假设的性能差异充其量是误导。
  • 它们在管理对内部可观察对象的订阅方面有所不同。在您可以通过 switchMap 实现嵌套订阅的情况下(仅发出一个值并且流完成的情况),没有任何有意义的内部可观察对象管理 - 在这种情况下基本上没有区别。除非我错过了什么?
  • @IngoBürk 我已经更新了我的答案,听起来不那么轻视switchMapmergeMap 之间的差异,而没有真正探讨任何差异
猜你喜欢
  • 1970-01-01
  • 2019-03-25
  • 1970-01-01
  • 1970-01-01
  • 2020-10-17
  • 1970-01-01
  • 1970-01-01
  • 2020-11-02
  • 1970-01-01
相关资源
最近更新 更多