tap()和subscribe()在设置类属性时有何区别?

59

我对rxjs非常陌生,只是想知道是否可以通过管道和tap来设置类属性,还是应该在订阅中进行。 对我来说,任何一种方式都可以,只是想知道是否可以按照我的意愿进行操作,或者是否有我不知道的问题。

演示两种方式的TypeScript代码:

export class ViewComponent implements OnInit {

  applicant = {};

  constructor(public route: ActivatedRoute, private store: Store<any>) {}

  ngOnInit() {
    this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => state.applicants.entities[params.get('id')])),
      tap(applicant => this.applicant = applicant)
    ).subscribe();
  }
}

对比

export class ViewComponent implements OnInit {

  applicant = {};

  constructor(public route: ActivatedRoute, private store: Store<any>) {}

  ngOnInit() {
    this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => state.applicants.entities[params.get('id')]))
    ).subscribe(applicant => this.applicant = applicant);
  }
}

有趣的问题。在处理路由参数时,我一直使用第二种方法,但并没有特别的原因。 - DeborahK
我已经投票将此问题关闭,因为它主要是基于个人观点的,但我会选择第二个。在行为上没有区别,但使用“tap”意味着您将导入实际上不需要的更多代码。 - cartant
但是这不是一个常见的操作符吗?我认为你最终会在某个地方导入它,在打包后它的大小不会有任何区别。在我的观点中,使用管道完成所有操作更易于理解,只是视觉上而已。 - bulforce
我对树摇和公共块等细节不是很确定 :/。这里有一个链接,解释了其中的原因:https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md#why - vince
我之所以链接它,是因为它提到了旧方法和新方法在树摇方面的差异。我认为@cartant所说的导入比你需要的代码更多的意思是,任何你可以用tap做的事情都可以在subscribe中完成,因此在任何地方使用tap可能会是“实际上不需要的更多代码”。 - vince
显示剩余4条评论
4个回答

54

编辑:请忽略此回答!

以下是一个好的回答:https://dev59.com/g1UM5IYBdhLWcg3wMd8k#50882183

有关更多背景信息,请参见JMD在下面的评论!


好问题。在tap操作符的源代码中,这个注释基本上总结了它:

此操作符对于调试您的可观测对象以获得正确值或执行其他副作用非常有用。
注意: 这与Observable上的 subscribe 不同。如果未订阅由 do 返回的 Observable,则指定的 Observer 不会发生副作用。因此,do 仅仅是间谍现有的执行,并不像 subscribe 那样触发执行。

tap中运行的任何副作用都可能放在subscribe块中。 subscribe表明您有意主动使用源值,因为它表示“当此可观测对象发出时,我想将其值保存在applicants变量中”。tap操作符主要用于调试,但是它也可以用于运行副作用。

总的来说,优先使用subscribe块来运行副作用,使用tap进行调试,但请注意,如果需要,tap可以做更多事情。


3
当我需要向可观测对象添加强制性副作用时,我会使用tap。无论如何被订阅,它们都会被激活。 - Persk
26
我非常不同意。所有可观察对象都应该通过 tap 设置副作用,以便它们可以被返回并通过其他管道进一步修改。一旦你强制订阅,你就失去了进一步控制可观察对象的能力。在我看来,订阅应该总是为空。 - JMD
1
我可以看到这个可以与Angular的AsyncPipe一起使用。AsyncPipe旨在实现声明式逻辑,而无需担心订阅/取消订阅流。AsyncPipe会为您处理订阅过程。因此,假设您在模板“vm $”上使用了一个表单中使用的模型。该模型是可观察的,并且将由AsyncPipe处理,您不需要订阅。但是,当提交表单时,您希望使用当前模型的快照。允许“tap”操作符更新其他“vm”属性似乎很有用和适用。 - Ross Brasseaux
2
我同意使用subscribe作为运行副作用的默认位置的总体想法,但是有时需要使用tap,因为可观察对象的最终结果可能不再包含您想要的副作用值。例如,如果其中一个操作符是reduce或map,则您正在寻找的值可能已经消失了。 - GEMI
我认为这个答案已经过时了。如果你去https://rxjs.dev/api/operators/tap,它会说:`Tap is designed to allow the developer a designated place to perform side effects.`即使你按照上面答案中给出的链接,它也是这么说的。 - starcorn

36

tap在observable与其订阅者分离时非常有用。如果你有一个暴露observable的类,你可以使用tap来实现该类需要执行的副作用,当某人“监听”这个observable时。另一方面,当你从另一个类订阅它时,你可以使用subscribe从订阅者的角度实现副作用。

具有observable的类:

public dummyObservable: Observable<number> = from([1, 2, 3, 4, 5]).pipe(
  // Side effects, executed every time I emit a value
  // I don't know which side effects implements who subscribes to me
  tap( n => console.log("I'm emitting this value:", n) )
);

订阅功能的类:

ngOnInit(): void {
  this.dummyService.dummyObservable.subscribe(
    // Side effects, executed every time I receive a value
    // I don't know which side effects implements the observable
    data => console.log("I'm receiving this value: ", data)
  );
}

13

Michael Hladky建议将所有的副作用都放在tap操作符中,并在此处解释了原因。

我认为这通常是一个好主意,因为正如Michael所说,你可以将许多可观察对象合并成一个单一的订阅。

我不知道这是否会改善性能,但当你想要取消订阅时,只有一个订阅使得操作更容易。取消订阅是你总是应该遵循的规则,以避免可能的内存泄漏或其他奇怪的行为。

这种方法的另一个好处是,你可以轻松地通过管道中的操作符(例如过滤器或takeWhile)暂停、恢复或完成一组可观察对象,或者通过另一个可观察对象进行switchmapping,就像这样:

const allMergedObservables$ = merge(obs1, obs2, obs3);
const play$ = new Subject();

play$.asObservable().pipe(
    switchMap(bool => bool ? allMergedObservables$ : EMPTY)
).subscribe();

// Putting 'true' into the play stream activates allMergedObservables$. 
play$.next(true);

// Something happens that makes you want to pause the application,
// for instance the user opens the print dialog box,
// so you issue 'false' in the play stream which in turn stops the
// inner subscription of allMergedObservables$:
play$.next(false);

然而,这取决于你和你喜欢的编程风格。

5

使用AsyncPipeNgrxPushPipe

对于那些想要使用async管道或ngrxPush管道的用户,则必须使用tap操作符。

在上面的示例中,我们可以得到类似以下内容:

export class ViewComponent implements OnInit {
  
  applicant = {};
  applicant$ = this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => state.applicants.entities[params.get('id')])),
      tap(applicant => this.applicant = applicant)
    )
  constructor(public route: ActivatedRoute, private store: Store<any>) {}

  ngOnInit() { }
}

在HTML中使用 AsyncPipe

<ng-container *ngIf='applicant$ | async'>

   ...Some Html code here

</ng-container>

使用 NgrxPushPipe(请记住,只有在导入 { ReactiveComponent } from '@ngrx/component' 时才有效)

<ng-container *ngIf='applicant$ | ngrxPush'>
 
...Some Html code here

</ng-container>

新增内容

上述的两个管道有助于提高代码可维护性,并减少/消除由于未取消订阅的可观察对象而导致的内存泄漏风险。

上述代码可以简化为:

TS 文件

export class ViewComponent {
 
  applicant$ = this.route.paramMap.pipe(
      switchMap(params => this.store.select(state => 
        state.applicants.entities[params.get('id')])
      )
    )
  constructor(public route: ActivatedRoute, private store: Store<any>) {}

}

HTML文件

使用AsyncPipe

<ng-container *ngIf='applicant$ | async as applicant'>

   ...Some Html code here

</ng-container>

使用 NgrxPushPipe
<ng-container *ngIf='applicant$ | ngrxPush as applicant'>

   ...Some Html code here

</ng-container>

使用 NgrxPushPipe
<ng-container *ngIf='applicant$ | ngrxPush as applicant'>

   ...Some Html code here

</ng-container>

使用“@ngrx/component”中的“ngrxLet”结构指令。
<ng-container *ngrxLet='applicant$; let applicant'>

   ...Some Html code here

</ng-container>

请问您能否提供一下您对这个问题的见解:https://dev59.com/wMDqa4cB1Zd3GeqPbFQs - Bravo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接