Angular 服务已被销毁,为什么订阅仍然从 Observable 中接收到数据?

4
使用 Angular,我有一个在组件中提供的服务。该服务具有一个可观察对象,组件已订阅此可观察对象。我原本以为不需要取消订阅该订阅/可观察对象,因为服务应随组件一起销毁,因此也应销毁可观察对象。然而,快速测试显示可观察对象仍然存在。
我的视线之外发生了什么?可观察对象是否在服务之外运行?或者当提供服务的组件被销毁时,服务实际上并没有被销毁?

2
当组件被销毁时,服务并不会停止存在。有用的资源 - Giovani Vercauteren
2
是的,为什么服务也应该被销毁?服务默认是单例的,它们不会在每次注入时生成。 - canbax
3
提供组件级别的服务将被销毁。这意味着它们可以实现 ngOnDestroy 钩子。但这并不意味着它会自动清除所有订阅,特别是如果我们在其中使用 setInterval 创建 Observable,并且没有返回 clearInterval 的清除方法。 - yurzui
@yurzui 你说得对。默认情况下,它们是在根级别提供的。我不太了解如何在组件级别提供它们。 - canbax
1
@canbax 我注意到在问题中 In my component ... providers: ... - yurzui
显示剩余3条评论
5个回答

0
如果您订阅一个不会完成的可观察对象,那么就会出现内存泄漏。即使服务由组件提供,您也在服务中创建了一个可观察对象并将其返回给了组件。现在,该可观察对象由组件引用,并且与创建它的服务无关。当组件被销毁时,它将被标记为垃圾回收,但仍然存在于内存中,直到垃圾回收器清理资源。您仍然需要取消订阅不会完成的可观察对象。
有几个选项:
1. 使用异步管道,因为它将为您管理订阅并取消订阅。 2. 保留对订阅的引用,并在 ngOnDestroy 上调用 unsubscribe。 3. 使用 takeUntil 和一个主题,使该主题发出一个 ngOnDestroy。
takeUntil 的好处是您可以使用它来管理多个订阅,而不必单独跟踪每个订阅。
1:异步管道
data$ = this.testService.testObservable();

并在视图中

<ng-container *ngIf="data$ | async as data">
  {{ data | json }}
</ng-container>

2:取消订阅

this.subscription = this.testService.testObservable().subscribe(data => console.log(data));

ngOnDestroy() {
  if (this.subscription) {
    this.subscription.unsubscribe();
  }
}

3: takeUntil

finalise = new Subject<void>();

this.subscription = this.testService.testObservable().pipe(takeUntil(this.finalise)).subscribe(data => console.log(data));

ngOnDestroy() {
  this.finalise.next();
  this.finalise.complete();
}

0

使用 takeUntil 处理你的情况。你需要创建/初始化一个主题,该主题将在组件销毁时完成。

private unsubscribe: Subject<void> = new Subject<void>();


this.testService.testObservable().pipe(takeUntil(this.unsubscribe))
.subscribe(data => console.log(data));

在组件销毁时完成订阅

使用一个主题和 takeUntil,您可以在 ngOnDestory 中一次性取消订阅多个可观察对象。

public ngOnDestroy(): void {
    this.unsubscribe.next();
    this.unsubscribe.complete();
  }

但是我仍然需要在每个订阅了该可观察对象的组件中取消订阅,对吗? - Superman.Lopez
是的,因为它们是独立的订阅。 - Ankit Kapoor

0
当我们返回或使用间隔时,可观察对象将持续接收流,直到我们停止它。为了最佳实践,您应该使用间隔可观察对象和 RxJS 操作符来自动销毁订阅。
// RxJS v6+
import { interval, timer } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

//emit value every 1s
const source = interval(1000);
//after 5 seconds, emit value
const timer$ = timer(5000);
//when timer emits after 5s, complete source
const example = source.pipe(takeUntil(timer$));
//output: 0,1,2,3
const subscribe = example.subscribe(val => console.log(val));

[注意] 如果您正在使用 setInterval,则需要使用 ngOnDestroy 显式取消订阅。

我提出的计时器只是为了检查可观察对象是否仍然存在。 - Superman.Lopez
如果您的服务是单例模式,那么只有在使用 providerIn: root 时才会出现这种情况。 - Ramneek Sharma
你是说我的可观察对象没有被销毁,只是因为我将setInterval实现为测试用例吗?或者其他可观察对象在销毁服务时也会保持活动状态? - Superman.Lopez
不,我在谈论用例。您的服务是否在根部提供,并且行为像单例模式一样。在这种情况下,您的可观察订阅将保持持续有效。 - Ramneek Sharma

-1

首先要注意的是,Angular服务是单例并且不会被销毁。

其次,您的可观察对象是在组件中订阅而不是在服务中订阅。 最后回答您的问题,

当组件被销毁时,订阅不会被销毁。 您需要手动取消订阅。 有几种方法可以做到这一点,最简单的方法是

- 不要订阅,而是在模板中使用Async管道。

阅读此博客文章以了解更多有关在组件上取消所有订阅的更好方法


5
如果在组件级别提供了它,那么它会被销毁。 - yurzui
2
我不喜欢这个评论“不要订阅,而是在模板中使用Async管道。”有很多情况下,无法使用异步管道。 - Giovani Vercauteren
是的,有些情况下我们无法使用异步管道,对于这些订阅,我们必须确保在ngDestroy上取消订阅。 - Tarun1704
如果它们在根级别提供,则它们是单例对象。 - canbax
首先需要注意的是,Angular服务是单例的,并且不会被销毁。如果你不知道自己在说什么,请不要发表愚蠢的评论。你可以在组件中提供一个服务,当组件被销毁时,该服务也会被销毁,这个服务不是单例的。请回到文档中学习,然后再在stackoverflow上发表评论,一些初学者可能会把这些答案当作理所当然。 - LarryP

-1

根据yurzui的评论,我得出以下结论:

尽管服务已被销毁(因为它是在被销毁的组件中提供的),但可观察对象和订阅仍然在服务和组件之外继续工作。我假设这些不会在某个时刻被收集,因此我明确地清理它们。

我的问题不是如何取消订阅可观察对象,但无论如何,我认为分享我实际解决方案来管理我预期将被销毁的订阅是有用的。我在服务上创建了一个destroy()函数,父组件在ngOnDestroy周期中调用该函数。在此函数中,我从服务中的所有无限可观察对象发出完成信号。这样可以避免我在所有子组件中重复自己并取消订阅。

在我的服务中:

private subject = new BehaviorSubject<string>(null);

public testObservable(): Observable<string> {
    // ... 
    this.subject.next('result');
    return this.subject.asObservable();
}

destroy() {
    this.subject.complete();
}

在我的组件中:
ngOnInit() {
   this.testService.testObservable().subscribe(data => console.log(data));
}

ngOnDestroy() {
    this.testService.destroy();
}

编辑

我已经包含了一个可工作的 StackBlitz,以防我的解决方案有些不确定:https://stackblitz.com/edit/destroyservice。我喜欢它的原因是我可以用3行代码取消订阅6个订阅,并且无需在任何子组件中包含 ngOnDestroy。


这不是服务的解决方案,服务是可重用的,而这使它只能使用一次。一旦被销毁,称为服务的对象将无法在下一个注入请求中工作。 - Adrian Brand
这是不正确的,正如我在问题和答案中所述,该服务是在组件中提供的,并且下次初始化组件时它将是可重用的。 - Superman.Lopez
一个服务应该被设计成可重用的,假设它将在组件中提供是一种糟糕的设计。你可能会认为把逻辑放在组件中更好,因为服务是为共享对象而设计的。 - Adrian Brand
为什么在组件中提供服务会是一个不好的设计?如果真的不好,那么为什么Angular的设计是在组件级别提供服务呢?此外,该服务是可重用的,它的实例被用于父组件和嵌套组件。 - Superman.Lopez
在组件中提供服务是可以的,但我认为服务的设计是一个糟糕的设计。 - Adrian Brand
我的服务针对于特定的组件和子组件,在这些组件中使用的可观察对象是特定于此组件的。它被提供为主组件及其子组件的单个实例。通过将服务注入到需要对象的组件中(而不是注入到不需要它的组件中),我可以共享对象。在我看来,这是如何使用服务的好例子。你能详细说明一下为什么该服务的设计不好吗? - Superman.Lopez

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