简体   繁体   中英

Angular Service destroyed, but why is subscription still receiving from observable?

Using Angular, I have a service that is provided in a component . The service has an observable and the component is subscribed to this observable. I had expected I didn't need to unsubscribe from the subscription/observable as the service should be destroyed with the component and thus also the observable. However a quick test shows the observable to be alive.

What is happening out of my sight? Does the observable run outside the service? Or does the service not actually get destroyed when the component in which it was provided gets destroyed?

First point to note is that an Angular Service is singleton and does NOT get destroyed.

Secondly your observable is subscribed in your Component and not in your Service. and finally to answer your question,

The subscriptions are not destroyed when the the component is destroyed. You will have to manually unsubscribe. There are several ways to do this, easiest being

- Dont subscribe, instead use Async pipe in your template.

Read this blog post in medium to know more on better ways to unsubscribe all your subscriptions on the component

You have a memory leak if you subscribe to an observable that does not complete. Even if the service is provided by the component, you created an observable in the service and returned it to the component. Now the observable is referenced by the component and has nothing to do with the service that created it any more. When the component is destroyed it will be marked for garbage collection but still exists in memory until the garbage collector cleans up the resources. You still need to unsubscribe from observables that do not complete.

There are a few options

  1. Use the async pipe as it will manage subscriptions for you and unsubscribe
  2. Keep a reference to the subscription and call unsubscribe on ngOnDestroy
  3. Use take until with a subject and make the subject emit an ngOnDestroy.

The nice thing about takeUntil is you can use it to manage multiple subscriptions instead of having to track each subscription individually.

1: async pipe

data$ = this.testService.testObservable();

and in the view

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

2: unsubscribe

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();
}

Use takeUntil for your scenario. You need to create/initialize a subject which will be completed on component destroy.

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


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

Complete subscription on component destruction

Using a subject and takeUntil, you can unsubscribe to multiple observables in one go at ngOnDestory.

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

When we are returning or using intervals then observable will continuously receive stream until we stop it. For best practice you should use interval observable and RxJS operator to automatic destroy the subscription.

// 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));

[Note] if you are using setInterval then you need to explicitly unsubscribe the subscription using ngOnDestroy

Following the comments of yurzui I conclude the following:

Despite the service being destroyed (as it was provided in the component that got destroyed) the observable and subscription continue to work outside the service and component. I assume this doesn't get collected at some point, so I explicitly clean it up.

My question was not how to unsubscribe from the observable, but regardless I thought it would be useful to share my actual solution to managing the subscriptions I had expected to be destroyed. I created a destroy() function on the service which the parent component calls in the ngOnDestroy cycle. In this function I emit complete from all infinite observables in the service. This saves me from repeating myself and unsubscribing in all the child components.

In my service:

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

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

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

In my component:

ngOnInit() {
   this.testService.testObservable().subscribe(data => console.log(data));
}

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

Edit

I have included a working stackblitz in case there is some uncertainty about what my solution is: https://stackblitz.com/edit/destroyservice . What I like about it is that I unsubscribe from 6 subscriptions with 3 lines of code, and I don't need to include ngOnDestroy in any of the child components.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM