Angular/RxJs When should I unsubscribe from `Subscription`

前端 未结 22 2183
隐瞒了意图╮
隐瞒了意图╮ 2020-11-21 04:56

When should I store the Subscription instances and invoke unsubscribe() during the NgOnDestroy life cycle and when can I simply ignore them?

相关标签:
22条回答
  • 2020-11-21 05:12

    It depends. If by calling someObservable.subscribe(), you start holding up some resource that must be manually freed-up when the lifecycle of your component is over, then you should call theSubscription.unsubscribe() to prevent memory leak.

    Let's take a closer look at your examples:

    getHero() returns the result of http.get(). If you look into the angular 2 source code, http.get() creates two event listeners:

    _xhr.addEventListener('load', onLoad);
    _xhr.addEventListener('error', onError);
    

    and by calling unsubscribe(), you can cancel the request as well as the listeners:

    _xhr.removeEventListener('load', onLoad);
    _xhr.removeEventListener('error', onError);
    _xhr.abort();
    

    Note that _xhr is platform specific but I think it's safe to assume that it is an XMLHttpRequest() in your case.

    Normally, this is enough evidence to warrant a manual unsubscribe() call. But according this WHATWG spec, the XMLHttpRequest() is subject to garbage collection once it is "done", even if there are event listeners attached to it. So I guess that's why angular 2 official guide omits unsubscribe() and lets GC clean up the listeners.

    As for your second example, it depends on the implementation of params. As of today, the angular official guide no longer shows unsubscribing from params. I looked into src again and found that params is a just a BehaviorSubject. Since no event listeners or timers were used, and no global variables were created, it should be safe to omit unsubscribe().

    The bottom line to your question is that always call unsubscribe() as a guard against memory leak, unless you are certain that the execution of the observable doesn't create global variables, add event listeners, set timers, or do anything else that results in memory leaks.

    When in doubt, look into the implementation of that observable. If the observable has written some clean up logic into its unsubscribe(), which is usually the function that is returned by the constructor, then you have good reason to seriously consider calling unsubscribe().

    0 讨论(0)
  • 2020-11-21 05:12

    in SPA application at ngOnDestroy function (angular lifeCycle) For each subscribe you need to unsubscribe it. advantage => to prevent the state from becoming too heavy.

    for example: in component1 :

    import {UserService} from './user.service';
    
    private user = {name: 'test', id: 1}
    
    constructor(public userService: UserService) {
        this.userService.onUserChange.next(this.user);
    }
    

    in service:

    import {BehaviorSubject} from 'rxjs/BehaviorSubject';
    
    public onUserChange: BehaviorSubject<any> = new BehaviorSubject({});
    

    in component2:

    import {Subscription} from 'rxjs/Subscription';
    import {UserService} from './user.service';
    
    private onUserChange: Subscription;
    
    constructor(public userService: UserService) {
        this.onUserChange = this.userService.onUserChange.subscribe(user => {
            console.log(user);
        });
    }
    
    public ngOnDestroy(): void {
        // note: Here you have to be sure to unsubscribe to the subscribe item!
        this.onUserChange.unsubscribe();
    }
    
    0 讨论(0)
  • 2020-11-21 05:13

    --- Edit 4 - Additional Resources (2018/09/01)

    In a recent episode of Adventures in Angular Ben Lesh and Ward Bell discuss the issues around how/when to unsubscribe in a component. The discussion starts at about 1:05:30.

    Ward mentions right now there's an awful takeUntil dance that takes a lot of machinery and Shai Reznik mentions Angular handles some of the subscriptions like http and routing.

    In response Ben mentions that there are discussions right now to allow Observables to hook into the Angular component lifecycle events and Ward suggests an Observable of lifecycle events that a component could subscribe to as a way of knowing when to complete Observables maintained as component internal state.

    That said, we mostly need solutions now so here are some other resources.

    1. A recommendation for the takeUntil() pattern from RxJs core team member Nicholas Jamieson and a tslint rule to help enforce it. https://ncjamieson.com/avoiding-takeuntil-leaks/

    2. Lightweight npm package that exposes an Observable operator that takes a component instance (this) as a parameter and automatically unsubscribes during ngOnDestroy. https://github.com/NetanelBasal/ngx-take-until-destroy

    3. Another variation of the above with slightly better ergonomics if you are not doing AOT builds (but we should all be doing AOT now). https://github.com/smnbbrv/ngx-rx-collector

    4. Custom directive *ngSubscribe that works like async pipe but creates an embedded view in your template so you can refer to the 'unwrapped' value throughout your template. https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

    I mention in a comment to Nicholas' blog that over-use of takeUntil() could be a sign that your component is trying to do too much and that separating your existing components into Feature and Presentational components should be considered. You can then | async the Observable from the Feature component into an Input of the Presentational component, which means no subscriptions are necessary anywhere. Read more about this approach here

    --- Edit 3 - The 'Official' Solution (2017/04/09)

    I spoke with Ward Bell about this question at NGConf (I even showed him this answer which he said was correct) but he told me the docs team for Angular had a solution to this question that is unpublished (though they are working on getting it approved). He also told me I could update my SO answer with the forthcoming official recommendation.

    The solution we should all use going forward is to add a private ngUnsubscribe = new Subject(); field to all components that have .subscribe() calls to Observables within their class code.

    We then call this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); in our ngOnDestroy() methods.

    The secret sauce (as noted already by @metamaker) is to call takeUntil(this.ngUnsubscribe) before each of our .subscribe() calls which will guarantee all subscriptions will be cleaned up when the component is destroyed.

    Example:

    import { Component, OnDestroy, OnInit } from '@angular/core';
    // RxJs 6.x+ import paths
    import { filter, startWith, takeUntil } from 'rxjs/operators';
    import { Subject } from 'rxjs';
    import { BookService } from '../books.service';
    
    @Component({
        selector: 'app-books',
        templateUrl: './books.component.html'
    })
    export class BooksComponent implements OnDestroy, OnInit {
        private ngUnsubscribe = new Subject();
    
        constructor(private booksService: BookService) { }
    
        ngOnInit() {
            this.booksService.getBooks()
                .pipe(
                   startWith([]),
                   filter(books => books.length > 0),
                   takeUntil(this.ngUnsubscribe)
                )
                .subscribe(books => console.log(books));
    
            this.booksService.getArchivedBooks()
                .pipe(takeUntil(this.ngUnsubscribe))
                .subscribe(archivedBooks => console.log(archivedBooks));
        }
    
        ngOnDestroy() {
            this.ngUnsubscribe.next();
            this.ngUnsubscribe.complete();
        }
    }
    

    Note: It's important to add the takeUntil operator as the last one to prevent leaks with intermediate observables in the operator chain.

    --- Edit 2 (2016/12/28)

    Source 5

    The Angular tutorial, the Routing chapter now states the following: "The Router manages the observables it provides and localizes the subscriptions. The subscriptions are cleaned up when the component is destroyed, protecting against memory leaks, so we don't need to unsubscribe from the route params Observable." - Mark Rajcok

    Here's a discussion on the Github issues for the Angular docs regarding Router Observables where Ward Bell mentions that clarification for all of this is in the works.

    --- Edit 1

    Source 4

    In this video from NgEurope Rob Wormald also says you do not need to unsubscribe from Router Observables. He also mentions the http service and ActivatedRoute.params in this video from November 2016.

    --- Original Answer

    TLDR:

    For this question there are (2) kinds of Observables - finite value and infinite value.

    http Observables produce finite (1) values and something like a DOM event listener Observables produce infinite values.

    If you manually call subscribe (not using async pipe), then unsubscribe from infinite Observables.

    Don't worry about finite ones, RxJs will take care of them.

    Source 1

    I tracked down an answer from Rob Wormald in Angular's Gitter here.

    He states (i reorganized for clarity and emphasis is mine)

    if its a single-value-sequence (like an http request) the manual cleanup is unnecessary (assuming you subscribe in the controller manually)

    i should say "if its a sequence that completes" (of which single value sequences, a la http, are one)

    if its an infinite sequence, you should unsubscribe which the async pipe does for you

    Also he mentions in this youtube video on Observables that they clean up after themselves... in the context of Observables that complete (like Promises, which always complete because they are always producing 1 value and ending - we never worried about unsubscribing from Promises to make sure they clean up xhr event listeners, right?).

    Source 2

    Also in the Rangle guide to Angular 2 it reads

    In most cases we will not need to explicitly call the unsubscribe method unless we want to cancel early or our Observable has a longer lifespan than our subscription. The default behavior of Observable operators is to dispose of the subscription as soon as .complete() or .error() messages are published. Keep in mind that RxJS was designed to be used in a "fire and forget" fashion most of the time.

    When does the phrase our Observable has a longer lifespan than our subscription apply?

    It applies when a subscription is created inside a component which is destroyed before (or not 'long' before) the Observable completes.

    I read this as meaning if we subscribe to an http request or an observable that emits 10 values and our component is destroyed before that http request returns or the 10 values have been emitted, we are still ok!

    When the request does return or the 10th value is finally emitted the Observable will complete and all resources will be cleaned up.

    Source 3

    If we look at this example from the same Rangle guide we can see that the Subscription to route.params does require an unsubscribe() because we don't know when those params will stop changing (emitting new values).

    The component could be destroyed by navigating away in which case the route params will likely still be changing (they could technically change until the app ends) and the resources allocated in subscription would still be allocated because there hasn't been a completion.

    0 讨论(0)
  • 2020-11-21 05:13

    Angular 2 official documentation provides an explanation for when to unsubscribe and when it can be safely ignored. Have a look at this link:

    https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service

    Look for the paragraph with the heading Parent and children communicate via a service and then the blue box:

    Notice that we capture the subscription and unsubscribe when the AstronautComponent is destroyed. This is a memory-leak guard step. There is no actual risk in this app because the lifetime of a AstronautComponent is the same as the lifetime of the app itself. That would not always be true in a more complex application.

    We do not add this guard to the MissionControlComponent because, as the parent, it controls the lifetime of the MissionService.

    I hope this helps you.

    0 讨论(0)
  • 2020-11-21 05:15

    Another short addition to the above mentioned situations is:

    • Always unsubscribe, when new values in the subscribed stream is no more required or don't matter, it will result in way less number of triggers and increase in performance in a few cases. Cases such as components where the subscribed data/event no more exists or a new subscription to an all new stream is required (refresh, etc.) is a good example for unsubscription.
    0 讨论(0)
  • 2020-11-21 05:15

    A Subscription essentially just has an unsubscribe() function to release resources or cancel Observable executions. In Angular, we have to unsubscribe from the Observable when the component is being destroyed. Luckily, Angular has a ngOnDestroy hook that is called before a component is destroyed, this enables devs to provide the cleanup crew here to avoid hanging subscriptions, open portals, and what nots that may come in the future to bite us in the back

    @Component({...})
    export class AppComponent implements OnInit, OnDestroy {
        subscription: Subscription 
        ngOnInit () {
            var observable = Rx.Observable.interval(1000);
            this.subscription = observable.subscribe(x => console.log(x));
        }
        ngOnDestroy() {
            this.subscription.unsubscribe()
        }
    }
    

    We added ngOnDestroy to our AppCompoennt and called unsubscribe method on the this.subscription Observable

    If there are multiple subscriptions:

    @Component({...})
    export class AppComponent implements OnInit, OnDestroy {
        subscription1$: Subscription
        subscription2$: Subscription 
        ngOnInit () {
            var observable1$ = Rx.Observable.interval(1000);
            var observable2$ = Rx.Observable.interval(400);
            this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x));
            this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x));
        }
        ngOnDestroy() {
            this.subscription1$.unsubscribe()
            this.subscription2$.unsubscribe()
        }
    }
    
    0 讨论(0)
提交回复
热议问题