Question Angular / RxJs Quand devrais-je me désabonner de `Subscription`


Quand dois-je stocker le Subscription instances et invoquer unsubscribe() pendant le cycle de vie NgOnDestroy et quand puis-je simplement les ignorer?

L'enregistrement de tous les abonnements introduit beaucoup de désordre dans le code du composant.

Guide du client HTTP ignorer les abonnements comme ceci:

getHeroes() {
  this.heroService.getHeroes()
                   .subscribe(
                     heroes => this.heroes = heroes,
                     error =>  this.errorMessage = <any>error);
}

Dans le même temps Route et guide de navigation dit ça:

Finalement, nous naviguerons ailleurs. Le routeur supprimera ce composant du DOM et le détruira. Nous devons nettoyer après nous-mêmes avant que cela n'arrive. Plus précisément, nous devons nous désabonner avant que Angular ne détruise le composant. Ne pas le faire pourrait créer une fuite de mémoire.

Nous nous désabonnons de notre Observable dans le ngOnDestroy méthode.

private sub: any;

ngOnInit() {
  this.sub = this.route.params.subscribe(params => {
     let id = +params['id']; // (+) converts string 'id' to a number
     this.service.getHero(id).then(hero => this.hero = hero);
   });
}

ngOnDestroy() {
  this.sub.unsubscribe();
}

419
2018-06-24 07:52


origine


Réponses:


--- Edit 3 - La solution "officielle" (2017/04/09)

J'ai parlé avec Ward Bell de cette question à NGConf (je lui ai même montré cette réponse qu'il a dite correcte) mais il m'a dit que l'équipe de docs pour Angular avait une solution à cette question non publiée (bien qu'ils travaillent pour la faire approuver) ). Il m'a également dit que je pouvais mettre à jour ma réponse SO avec la recommandation officielle à venir.

La solution que nous devrions tous utiliser à l'avenir est d'ajouter un private ngUnsubscribe: Subject = new Subject(); champ à tous les composants qui ont .subscribe() appels à Observables dans leur code de classe.

Nous appelons alors this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); dans notre ngOnDestroy() méthodes

La sauce secrète (comme déjà noté par @metamaker) est d'appeler .takeUntil(this.ngUnsubscribe) avant chacun de nos .subscribe() les appels qui garantiront tous les abonnements seront nettoyés lorsque le composant sera détruit.

Exemple:

import { Component, OnDestroy, OnInit } from '@angular/core';
import 'rxjs/add/operator/takeUntil';
// import { takeUntil } from 'rxjs/operators'; // for rxjs ^5.5.0 lettable operators
import { Subject } from 'rxjs/Subject';
import { MyThingService } from '../my-thing.service';

@Component({
    selector: 'my-thing',
    templateUrl: './my-thing.component.html'
})
export class MyThingComponent implements OnDestroy, OnInit {
    private ngUnsubscribe: Subject = new Subject();

    constructor(
        private myThingService: MyThingService,
    ) { }

    ngOnInit() {
        this.myThingService.getThings()
            .takeUntil(this.ngUnsubscribe)
            .subscribe(things => console.log(things));

        /* if using lettable operators in rxjs ^5.5.0
        this.myThingService.getThings()
            .pipe(takeUntil(this.ngUnsubscribe))
            .subscribe(things => console.log(things)); */

        this.myThingService.getOtherThings()
            .takeUntil(this.ngUnsubscribe)
            .subscribe(things => console.log(things));
    }

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

Remarque: Il est important d'ajouter le takeUntil opérateur comme le dernier à prévenir les fuites avec des observables intermédiaires dans la chaîne de l'opérateur.

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

Source 5

Le tutoriel Angular, le chapitre Routing, indique ce qui suit: "Le routeur gère les observables qu'il fournit et localise les abonnements.Les abonnements sont nettoyés lorsque le composant est détruit, protégeant contre les fuites de mémoire, donc nous ne devons pas nous désabonner de la route params Observable. " - Mark Rajcok

Voici un discussion sur les questions Github pour les docs Angular concernant les Observables Routeurs où Ward Bell mentionne que des clarifications pour tout cela sont en cours.

--- Modifier 1

Source 4

Dans ce vidéo de NgEurope Rob Wormald dit aussi que vous n'avez pas besoin de vous désabonner de Router Observables. Il mentionne également le http service et ActivatedRoute.params dans ce vidéo de novembre 2016.

--- Réponse originale

TLDR:

Pour cette question, il existe deux types de Observables - fini valeur et infini valeur.

http  Observables produire fini (1) valeurs et quelque chose comme un DOM event listener  Observables produire infini valeurs.

Si vous appelez manuellement subscribe (ne pas utiliser de tuyau asynchrone), alors unsubscribede infini  Observables.

Ne t'inquiète pas pour fini ceux, RxJs prendra soin d'eux.

Source 1

J'ai trouvé une réponse de Rob Wormald dans Angular's Gitter ici.

Il déclare (je me suis réorganisé pour la clarté et l'emphase est le mien)

si c'est une séquence à valeur unique (comme une requête http)   la le nettoyage manuel est inutile (en supposant que vous vous abonnez dans le contrôleur manuellement)

je devrais dire "si c'est un séquence qui se termine"(dont les séquences à valeur unique, a la http, en sont une)

si c'est une séquence infinie, vous devriez vous désabonner que le tuyau async fait pour vous

Aussi, il mentionne à ce sujet vidéo Youtube sur les observables they clean up after themselves... dans le contexte des observables complete (comme les Promesses, qui sont toujours complètes parce qu'elles produisent toujours une valeur et se terminent - nous ne nous sommes jamais souciés de nous désinscrire des Promesses pour nous assurer qu'elles nettoyaient xhr auditeurs d'événements, non?).

Source 2

Aussi dans le Guide Rangle à Angular 2 ça lit

Dans la plupart des cas, nous n'aurons pas besoin d'appeler explicitement la méthode de désabonnement à moins que nous ne souhaitions annuler tôt ou que notre Observable ait une durée de vie plus longue que notre abonnement. Le comportement par défaut des opérateurs Observable est de disposer de l'abonnement dès que les messages .complete () ou .error () sont publiés. Gardez à l'esprit que RxJS a été conçu pour être utilisé de manière «feu et oublier» la plupart du temps.

Quand la phrase our Observable has a longer lifespan than our subscription appliquer?

Il s'applique lorsqu'un abonnement est créé à l'intérieur d'un composant qui est détruit avant (ou pas "long" avant) le Observable complète.

Je lis ceci comme signifiant si nous nous abonnons à un http demande ou une observable qui émet 10 valeurs et notre composant est détruit avant que http demande de retour ou les 10 valeurs ont été émises, nous sommes toujours ok!

Lorsque la requête retourne ou la 10ème valeur est finalement émise le Observable se terminera et toutes les ressources seront nettoyées.

Source 3

Si on regarde cet exemple du même guide Rangle, nous pouvons voir que le Subscription à route.params nécessite un unsubscribe() parce que nous ne savons pas quand ceux params arrêtera de changer (en émettant de nouvelles valeurs).

Le composant pourrait être détruit en naviguant, auquel cas les paramètres d'itinéraire changeraient probablement (ils pourraient techniquement changer jusqu'à la fin de l'application) et les ressources allouées en abonnement seraient toujours attribuées parce qu'il n'y a pas eu de completion.


651
2017-12-16 04:11



Vous n'avez pas besoin d'avoir beaucoup d'abonnements et de vous désabonner manuellement. Utilisation RxJS.Subject et prendreUntil combo pour gérer les abonnements comme un boss:

import {Subject} from "rxjs/Subject";

@Component(
    {
        moduleId: __moduleName,
        selector: 'my-view',
        templateUrl: '../views/view-route.view.html',
    }
)
export class ViewRouteComponent implements OnDestroy
{
    componentDestroyed$: Subject<boolean> = new Subject();

    constructor(protected titleService: TitleService)
    {
        this.titleService.emitter1$
            .takeUntil(this.componentDestroyed$)
            .subscribe(
            (data: any) =>
            {
                // ... do something 1
            }
        );

        this.titleService.emitter2$
            .takeUntil(this.componentDestroyed$)
            .subscribe(
            (data: any) =>
            {
                // ... do something 2
            }
        );

        // ...

        this.titleService.emitterN$
            .takeUntil(this.componentDestroyed$)
            .subscribe(
            (data: any) =>
            {
                // ... do something N
            }
        );
    }

    ngOnDestroy()
    {
        this.componentDestroyed$.next(true);
        this.componentDestroyed$.complete();
    }
}

Approche alternative, qui a été proposé par @acumartini dans les commentaires, les usages prendre au lieu de prendreUntil. Vous pouvez le préférer, mais attention, de cette façon, votre exécution Observable ne sera pas annulée sur ngDestroy de votre composant (par exemple lorsque vous effectuez des calculs qui prennent beaucoup de temps ou attendez des données du serveur). Méthode basée sur prendreUntil, n'a pas cet inconvénient et conduit à l'annulation immédiate de la demande. Merci à @AlexChe pour une explication détaillée dans les commentaires.

Alors voici le code:

@Component(
    {
        moduleId: __moduleName,
        selector: 'my-view',
        templateUrl: '../views/view-route.view.html',
    }
)
export class ViewRouteComponent implements OnDestroy
{
    alive: boolean = true;

    constructor(protected titleService: TitleService)
    {
        this.titleService.emitter1$
            .takeWhile(() => this.alive)
            .subscribe(
            (data: any) =>
            {
                // ... do something 1
            }
        );

        this.titleService.emitter2$
            .takeWhile(() => this.alive)
            .subscribe(
            (data: any) =>
            {
                // ... do something 2
            }
        );

        // ...

        this.titleService.emitterN$
            .takeWhile(() => this.alive)
            .subscribe(
            (data: any) =>
            {
                // ... do something N
            }
        );
    }

    // Probably, this.alive = false MAY not be required here, because
    // if this.alive === undefined, takeWhile will stop. I
    // will check it as soon, as I have time.
    ngOnDestroy()
    {
        this.alive = false;
    }
}

58
2018-03-09 12:35



La classe d'abonnement a une propriété intéressante:

Représente une ressource jetable, telle que l'exécution d'un Observable. Un abonnement a une méthode importante, désabonnement, qui ne prend aucun argument et ne dispose que de la ressource détenue par l'abonnement.
De plus, les abonnements peuvent être regroupés via la méthode add (), qui joindra un abonnement enfant à l'abonnement en cours. Lorsqu'un Abonnement est désabonné, tous ses enfants (et ses petits-enfants) seront également désabonnés.

Vous pouvez créer un objet Abonnement agrégé qui regroupe tous vos abonnements. Pour ce faire, créez un abonnement vide et ajoutez-y des abonnements en utilisant add() méthode. Lorsque votre composant est détruit, il vous suffit de vous désabonner de l'abonnement global.

@Component({ ... })
export class SmartComponent implements OnInit, OnDestroy {
  private subscriptions = new Subscription();

  constructor(private heroService: HeroService) {
  }

  ngOnInit() {
    this.subscriptions.add(this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes));
    this.subscriptions.add(/* another subscription */);
    this.subscriptions.add(/* and another subscription */);
    this.subscriptions.add(/* and so on */);
  }

  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}

30
2018-05-03 12:00



Ça dépend. Si en appelant someObservable.subscribe(), vous commencez à retenir certaines ressources qui doivent être libérées manuellement lorsque le cycle de vie de votre composant est terminé, alors vous devriez appeler theSubscription.unsubscribe() pour empêcher la fuite de mémoire.

Jetons un coup d'œil à vos exemples:

getHero() renvoie le résultat de http.get(). Si vous regardez dans l'angle 2 code source, http.get() crée deux écouteurs d'événement:

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

et en appelant unsubscribe(), vous pouvez annuler la demande ainsi que les auditeurs:

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

Notez que _xhr est spécifique à la plate-forme, mais je pense qu'il est sûr de supposer que c'est un XMLHttpRequest() dans ton cas.

Normalement, ceci est une preuve suffisante pour justifier un manuel unsubscribe() appel. Mais selon cette WHATWG spec, la XMLHttpRequest() est sujet au ramassage des ordures une fois qu'il est "terminé", même s'il y a des écouteurs d'événement associés. Donc je suppose que c'est pourquoi le guide officiel angulaire 2 omet unsubscribe() et permet au GC de nettoyer les auditeurs.

Quant à votre deuxième exemple, cela dépend de la mise en œuvre de params. À compter d'aujourd'hui, le guide officiel angulaire ne montre plus désabonnement de params. J'ai regardé dans src encore et a constaté que params est juste un ComportementSubject. Étant donné qu’aucun programme d’écoute d’événement ou de temporisation n’a été utilisé, et qu’aucune variable globale n’a été créée, il convient de l’omettre en toute sécurité. unsubscribe().

La ligne de fond à votre question est que toujours appeler unsubscribe() en tant que protection contre les fuites de mémoire, sauf si vous êtes certain que l'exécution de l'observable ne crée pas de variables globales, n'ajoute pas d'écouteurs d'événement, ne règle pas les minuteries ou ne fait rien d'autre qui entraîne des fuites de mémoire.

En cas de doute, examinez la mise en œuvre de cet observable. Si l'observable a écrit une logique de nettoyage dans son unsubscribe(), qui est généralement la fonction renvoyée par le constructeur, alors vous avez de bonnes raisons d’envisager sérieusement d’appeler unsubscribe().


13
2017-12-01 07:09



La documentation officielle Angular 2 fournit une explication du moment où vous devez vous désabonner et quand il peut être ignoré en toute sécurité. Jetez un oeil à ce lien:

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

Recherchez le paragraphe avec l'en-tête Parent et enfants communiquent via un service puis la boîte bleue:

Notez que nous capturons l'abonnement et nous désabonnons lorsque l'AstronautComponent est détruit. C'est une étape de garde de mémoire-fuite. Il n'y a pas de risque réel dans cette application car la durée de vie d'un AstronautComponent est la même que la durée de vie de l'application elle-même. Ce ne serait pas toujours vrai dans une application plus complexe.

Nous n'ajoutons pas cette garde à MissionControlComponent car, en tant que parent, elle contrôle la durée de vie de MissionService.

J'espère que ceci vous aide.


5
2018-06-29 11:08



Puisque la solution de seangwright (Edit 3) semble très utile, j'ai aussi eu du mal à intégrer cette fonctionnalité dans le composant de base, et je suggère aux autres coéquipiers du projet d'appeler super () sur ngOnDestroy pour activer cette fonctionnalité.

Cette réponse fournit un moyen de libérer des super-appels et de faire de "componentDestroyed $" un composant de base de la base.

class BaseClass {
    protected componentDestroyed$: Subject<void> = new Subject<void>();
    constructor() {

        /// wrap the ngOnDestroy to be an Observable. and set free from calling super() on ngOnDestroy.
        let _$ = this.ngOnDestroy;
        this.ngOnDestroy = () => {
            this.componentDestroyed$.next();
            this.componentDestroyed$.complete();
            _$();
        }
    }

    /// placeholder of ngOnDestroy. no need to do super() call of extended class.
    ngOnDestroy() {}
}

Et puis vous pouvez utiliser cette fonctionnalité librement par exemple:

@Component({
    selector: 'my-thing',
    templateUrl: './my-thing.component.html'
})
export class MyThingComponent extends BaseClass implements OnInit, OnDestroy {
    constructor(
        private myThingService: MyThingService,
    ) { super(); }

    ngOnInit() {
        this.myThingService.getThings()
            .takeUntil(this.componentDestroyed$)
            .subscribe(things => console.log(things));
    }

    /// optional. not a requirement to implement OnDestroy
    ngOnDestroy() {
        console.log('everything works as intended with or without super call');
    }

}

3
2018-04-24 04:30



Basé sur : Utilisation de l'héritage de classe pour se connecter au cycle de vie des composants Angular 2

Une autre approche générique:

export abstract class UnsubscribeOnDestroy implements OnDestroy {
  protected d$: Subject<any>;

  constructor() {
    this.d$ = new Subject<void>();

    const f = this.ngOnDestroy;
    this.ngOnDestroy = () => {
      f();
      this.d$.next();
      this.d$.complete();
    };
  }

  public ngOnDestroy() {
    // no-op
  }

}

Et utilise :

@Component({
    selector: 'my-comp',
    template: ``
})
export class RsvpFormSaveComponent extends UnsubscribeOnDestroy implements OnInit {

    constructor() {
        super();
    }

    ngOnInit(): void {
      Observable.of('bla')
      .takeUntil(this.d$)
      .subscribe(val => console.log(val));
    }
}


3
2018-04-12 11:04



La réponse officielle d'Edit # 3 (et les variations) fonctionne bien, mais la chose qui me vient à l'esprit est la «confusion» de la logique métier autour de l'abonnement observable.

Voici une autre approche utilisant des wrappers.

Warining: code expérimental

Fichier subscribeAndGuard.ts est utilisé pour créer une nouvelle extension Observable à envelopper .subscribe() et à l'intérieur pour envelopper ngOnDestroy().
L'utilisation est la même que .subscribe(), à l'exception d'un premier paramètre supplémentaire référençant le composant.

import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';

const subscribeAndGuard = function(component, fnData, fnError = null, fnComplete = null) {

  // Define the subscription
  const sub: Subscription = this.subscribe(fnData, fnError, fnComplete);

  // Wrap component's onDestroy
  if (!component.ngOnDestroy) {
    throw new Error('To use subscribeAndGuard, the component must implement ngOnDestroy');
  }
  const saved_OnDestroy = component.ngOnDestroy;
  component.ngOnDestroy = () => {
    console.log('subscribeAndGuard.onDestroy');
    sub.unsubscribe();
    // Note: need to put original back in place
    // otherwise 'this' is undefined in component.ngOnDestroy
    component.ngOnDestroy = saved_OnDestroy;
    component.ngOnDestroy();

  };

  return sub;
};

// Create an Observable extension
Observable.prototype.subscribeAndGuard = subscribeAndGuard;

// Ref: https://www.typescriptlang.org/docs/handbook/declaration-merging.html
declare module 'rxjs/Observable' {
  interface Observable<T> {
    subscribeAndGuard: typeof subscribeAndGuard;
  }
}

Voici un composant avec deux abonnements, un avec le wrapper et un sans. La seule mise en garde est que doit implémenter OnDestroy (avec un corps vide si désiré), sinon Angular ne sait pas appeler la version enveloppée.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/Rx';
import './subscribeAndGuard';

@Component({
  selector: 'app-subscribing',
  template: '<h3>Subscribing component is active</h3>',
})
export class SubscribingComponent implements OnInit, OnDestroy {

  ngOnInit() {

    // This subscription will be terminated after onDestroy
    Observable.interval(1000)
      .subscribeAndGuard(this,
        (data) => { console.log('Guarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );

    // This subscription will continue after onDestroy
    Observable.interval(1000)
      .subscribe(
        (data) => { console.log('Unguarded:', data); },
        (error) => { },
        (/*completed*/) => { }
      );
  }

  ngOnDestroy() {
    console.log('SubscribingComponent.OnDestroy');
  }
}

Un démon plunker est ici

Une note supplémentaire: Re Edit 3 - La solution 'Officielle', ceci peut être simplifié en utilisant takeWhile () au lieu de takeUntil () avant les abonnements, et un simple booléen plutôt qu'un autre observable dans ngOnDestroy.

@Component({...})
export class SubscribingComponent implements OnInit, OnDestroy {

  iAmAlive = true;
  ngOnInit() {

    Observable.interval(1000)
      .takeWhile(() => { return this.iAmAlive; })
      .subscribe((data) => { console.log(data); });
  }

  ngOnDestroy() {
    this.iAmAlive = false;
  }
}

2
2018-05-16 23:06