web-dev-qa-db-fra.com

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

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

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

Guide du client HTTP ignore les abonnements comme ceci:

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

Dans le même temps Route & Navigation Guide dit que:

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 se produise. Plus précisément, nous devons nous désabonner avant que Angular ne détruise le composant. Sinon, vous risquez de provoquer une fuite de mémoire.

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

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

--- Édition 4 - Ressources supplémentaires (2018/09/01)

Dans un récent épisode de Adventures in Angular Ben Lesh et Ward Bell discutent des questions concernant le moment et le moment de se désabonner d'un composant. La discussion commence vers 1h05h30.

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

En réponse, Ben mentionne qu'il y a actuellement des discussions pour permettre à Observables de se connecter aux événements de cycle de vie du composant Angular et Ward suggère un observable d'événements de cycle de vie auquel un composant pourrait s'abonner afin de savoir quand compléter Observables. maintenu en tant qu'état interne de composant.

Cela dit, nous avons surtout besoin de solutions maintenant, alors voici quelques autres ressources.

  1. Une recommandation pour le motif takeUntil() de Nicholas Jamieson, membre de l'équipe principale de RxJs, et une règle tslint pour l'aider à l'appliquer. https://blog.angularindepth.com/rxjs-avoiding-takeuntil-leaks-fb5182d047ef

  2. Paquet npm léger qui expose un opérateur Observable qui prend une instance de composant (this) en tant que paramètre et qui se désabonne automatiquement pendant ngOnDestroy. https://github.com/NetanelBasal/ngx-take-until-destroy

  3. Une autre variante de ce qui précède avec une ergonomie légèrement meilleure si vous ne faites pas de builds AOT (mais nous devrions tous faire AOT maintenant). https://github.com/smnbbrv/ngx-rx-collector

  4. Directive personnalisée *ngSubscribe qui fonctionne comme un canal asynchrone, mais crée une vue intégrée dans votre modèle afin que vous puissiez faire référence à la valeur "non enveloppée" dans votre modèle. https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

Dans un commentaire sur le blog de Nicholas, je mentionne qu'une utilisation excessive de takeUntil() pourrait indiquer que votre composant tente de trop en faire et que la séparation de vos composants existants dans Featureet Les éléments de présentationdoivent être pris en compte. Vous pouvez ensuite | async l'observable du composant Feature dans un Input du composant Presentational, ce qui signifie qu'aucun abonnement n'est nécessaire nulle part. En savoir plus sur cette approche here

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

J'ai parlé à 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 documentation de Angular avait une solution inédite à cette question (bien qu'ils travaillent pour le faire approuver). Il m'a également dit que je pourrais mettre à jour ma réponse SO avec la prochaine recommandation officielle.

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

Nous appelons ensuite this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); dans nos méthodes ngOnDestroy().

La sauce secrète (comme noté déjà par @ metamaker ) consiste à appeler takeUntil(this.ngUnsubscribe) avant chacun de nos appels .subscribe(), ce qui garantira que tous les abonnements seront nettoyés lors de la destruction du composant. .

Exemple:

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

Remarque:Il est important d'ajouter l'opérateur takeUntil en dernier pour éviter les fuites avec des observables intermédiaires dans la chaîne d'opérateurs.

--- Édition 2 (2016/12/28)

Source 5

Dans le didacticiel Angular, le chapitre Routage 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, ce qui les protège contre les fuites de mémoire. Vous n'avez pas besoin de vous désabonner des paramètres de la route Observable. " - Mark Rajcok

Voici une discussion sur les problèmes de Github pour les documents Angular concernant les observateurs de routeur, où Ward Bell mentionne que des éclaircissements sont en cours.

--- Modifier 1

Source 4

Dans ce vidéo de NgEurope , Rob Wormald dit également que vous n’avez pas besoin de vous désabonner des observateurs de routeur. Il mentionne également le service http et ActivatedRoute.params dans ce vidéo de novembre 2016 .

--- Réponse originale

TLDR:

Pour cette question, il existe (2) types de Observables - finitevalue et infinitevalue.

httpObservables produire valeurs finies(1) et ressembler à un DOM event listenerObservables produire infinivalues.

Si vous appelez manuellement subscribe (sans utiliser le canal asynchrone), alors unsubscribe à partir de infiniteObservables.

Ne vous inquiétez pas pour finiteones, RxJs en prendra soin.

Source 1

J'ai retrouvé une réponse de Rob Wormald dans Gitter d'Angular ici .

Il déclare (je me suis réorganisé pour plus de clarté et l'accent est à moi)

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

je devrais dire "si c'est une séquence complétant" (dont les séquences à valeur unique, à la http, en sont une)

si c'est une séquence infinie, vous devez vous désabonnerce que le pipe async fait pour vous

Il mentionne également dans ce vidéo youtube sur Observables que they clean up after themselves... dans le contexte des Observables, complete (comme Promises, qui sont toujours complètes car elles produisent toujours une valeur et se terminent - nous ne vous inquiétez jamais de vous désabonner de Promises pour vous assurer de nettoyer les écouteurs d'événements xhr, n'est-ce pas?).

Source 2

Également dans le Guide de l’angle pour Angular 2 il lit

Dans la plupart des cas, nous n'aurons pas besoin d'appeler explicitement la méthode de désinscription à moins de vouloir annuler tôt ou si notre Observable a une durée de vie supérieure à celle de notre abonnement. Le comportement par défaut des opérateurs observables consiste à 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 incontrôlable la plupart du temps.

Quand la phrase our Observable has a longer lifespan than our subscription s'applique-t-elle?

Cela s'applique lorsqu'un abonnement est créé dans un composant qui est détruit avant (ou pas 'longtemps' avant) le Observable est terminé.

J'interprète cela si nous souscrivons à une demande http ou à un observable émettant 10 valeurs et que notre composant est détruit avant le retour de la demande http ou que les 10 valeurs ont été émises. Nous sommes toujours d'accord!

Lorsque la demande est renvoyée ou que la 10ème valeur est finalement émise, le Observable sera terminé et toutes les ressources seront nettoyées.

Source 3

Si nous regardons cet exemple du même guide Rangle, nous pouvons voir que le Subscription to route.params nécessite un unsubscribe() parce que nous ne savons pas quand ces params cesseront de changer. (émettant de nouvelles valeurs).

Le composant peut être détruit en quittant le site. Dans ce cas, les paramètres de route changeront probablement (ils pourront techniquement être modifiés jusqu'à la fin de l'application) et les ressources allouées dans l'abonnement seront toujours allouées, car il n'y a pas eu completion.

898
seangwright

Vous n'avez pas besoin d'avoir beaucoup d'abonnements et de vous désinscrire manuellement. Utilisez la combinaison Subject et takeUntil pour gérer les abonnements comme un patron:

import { Subject } from "rxjs"
import { takeUntil } from "rxjs/operators"

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

  constructor(private titleService: TitleService) {}

  ngOnInit() {
    this.titleService.emitter1$
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe((data: any) => { /* ... do something 1 */ })

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

    //...

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

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

Approche alternative , qui a été proposée par @acumartini dans les commentaires , utilise takeWhile au lieu de - takeUntil . Vous préférerez peut-être cela, mais sachez que de cette manière, votre exécution Observable ne sera pas annulée sur ngDestroy de votre composant (par exemple, lorsque vous effectuez des calculs fastidieux ou que vous attendez des données du serveur). La méthode, qui est basée sur takeUntil , ne présente pas cet inconvénient et entraîne l’annulation immédiate de la demande. Merci à @AlexChe pour l'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 OnInit, OnDestroy {
  alive: boolean = true

  constructor(private titleService: TitleService) {}

  ngOnInit() {
    this.titleService.emitter1$
      .pipe(takeWhile(() => this.alive))
      .subscribe((data: any) => { /* ... do something 1 */ })

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

    // ...

    this.titleService.emitterN$
      .pipe(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
  }
}
75
metamaker

La classe Subscription a une fonctionnalité intéressante:

Représente une ressource disponible, telle que l'exécution d'un observable. Un abonnement a une méthode importante, la désinscription, 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 à l'aide de la méthode add (), qui associe un abonnement enfant à l'abonnement actuel. Lorsqu'un abonnement n'est plus souscrit, tous ses enfants (et ses petits-enfants) seront également désabonné.

Vous pouvez créer un objet d'abonnement global qui regroupe tous vos abonnements. Vous faites cela en créant un Abonnement vide et en y ajoutant des abonnements en utilisant sa méthode add(). 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();
  }
}
60
Steven Liekens

Quelques unes des meilleures pratiques en matière de désabonnement d'observables dans les composants Angular:

Une citation de Routing & Navigation

Lorsque vous vous abonnez à un observable dans un composant, vous vous arrêtez presque toujours pour vous désabonner lorsque le composant est détruit.

Il y a quelques observables exceptionnels où ce n'est pas nécessaire. Les observables ActivatedRoute font partie des exceptions.

ActivatedRoute et ses observables sont isolés du routeur lui-même. Le routeur détruit un composant routé lorsqu'il n'est plus nécessaire et qu'ActivatedRoute injecté meurt avec lui.

N'hésitez pas à vous désinscrire quand même. C'est inoffensif et jamais une mauvaise pratique.

Et en répondant aux liens suivants:

J'ai rassemblé certaines des meilleures pratiques en matière de désabonnement d'observables à l'intérieur de Angular composants à partager avec vous:

  • La désinscription observable http est conditionnelle et nous devrions considérer les effets du "rappel d'abonnement" exécuté après la destruction du composant au cas par cas. Nous savons que angular se désabonne et nettoie l'observable http lui-même (1) , (2) . Bien que cela soit vrai du point de vue des ressources, il ne raconte que la moitié de l'histoire. Supposons que nous parlions d'appeler directement http depuis un composant, et que la réponse http prenait plus de temps que nécessaire, de sorte que l'utilisateur a fermé le composant. Le gestionnaire subscribe() sera toujours appelé, même si le composant est fermé et détruit. Cela peut avoir des effets secondaires indésirables et, dans le pire des cas, laisser l'état de l'application brisé. Cela peut également provoquer des exceptions si le code dans le rappel tente d'appeler quelque chose qui vient d'être éliminé. Cependant, dans le même temps, ils sont parfois souhaités. Par exemple, disons que vous créez un client de messagerie et que vous déclenchez un son lorsque l'envoi de l'e-mail est terminé. Vous souhaitez néanmoins que cela se produise même si le composant est fermé ( 8 ).
  • Pas besoin de vous désabonner des observables complets ou erronés. Cependant, cela ne fait pas de mal (7) .
  • Utilisez autant que possible AsyncPipe car il se désabonne automatiquement de l'observable lors de la destruction du composant.
  • Désabonnez-vous des observables ActivatedRoute tels que _route.params_ s'ils sont abonnés à un composant imbriqué (ajouté dans tpl avec le sélecteur de composant) ou dynamique, car ils peuvent être abonnés plusieurs fois tant que le composant parent/hôte existe. Pas besoin de vous désabonner d'eux dans d'autres scénarios comme mentionné dans la citation ci-dessus de Routing & Navigation docs.
  • Désabonnez-vous des observables globaux partagés entre des composants exposés via un service Angular, par exemple, car ils peuvent être abonnés plusieurs fois tant que le composant est initialisé.
  • Il n'est pas nécessaire de vous désabonner des observables internes d'un service d'application, car ce service n'est jamais détruit. À moins que votre application ne soit entièrement détruite, il n'y a aucune raison réelle de vous désabonner et aucune possibilité de fuite de mémoire. (6) .

    Remarque: En ce qui concerne les services étendus, c'est-à-dire les fournisseurs de composants, ils sont détruits lors de la destruction du composant. Dans ce cas, si nous nous abonnons à un observable de ce fournisseur, nous devrions envisager de le désabonner en utilisant le OnDestroy crochet de cycle de vie qui sera appelé lorsque le service sera détruit, en fonction du docs.
  • Utilisez une technique abstraite pour éviter tout désordre de code pouvant résulter de la désinscription. Vous pouvez gérer vos abonnements avec takeUntil(3) ou vous pouvez utiliser ceci npmpackage mentionné à ( 4) Le moyen le plus simple de vous désabonner des Observables dans Angular .
  • Toujours vous désabonner des observables FormGroup comme _form.valueChanges_ et _form.statusChanges_
  • Toujours vous désabonner des observables du service _Renderer2_ comme _renderer2.listen_
  • Désabonnez-vous de chaque observable en tant qu’étape de protection contre les fuites de mémoire jusqu’à Angular Docs nous indique explicitement quels observables sont inutiles pour être désabonnés (Problème de vérification: (5) Documentation de RxJS Désabonnement (Open ) ).
  • Bonus: utilisez toujours les méthodes Angular pour relier des événements tels que HostListener en tant que angular vous permet de supprimer les écouteurs d'événement si nécessaire et empêche toute fuite de mémoire potentielle due aux liaisons d'événement.

Un dernier conseil intéressant : Si vous ne savez pas si un observable est automatiquement désabonné/complété ou non, ajoutez un rappel complete à subscribe(...) et vérifiez s'il est appelé lorsque le composant est détruit.

26
Mouneer

Ça dépend. Si, en appelant someObservable.subscribe(), vous commencez à retenir certaines ressources qui doivent être libérées manuellement une fois le cycle de vie de votre composant terminé, vous devez appeler theSubscription.unsubscribe() pour éviter les fuites de mémoire.

Regardons de plus près vos exemples:

getHero() renvoie le résultat de http.get(). Si vous regardez dans le angular 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 prudent de supposer que c’est une XMLHttpRequest() dans votre cas.

Normalement, cela constitue une preuve suffisante pour justifier un appel manuel unsubscribe(). Mais selon ce WHATWG spec , la XMLHttpRequest() est sujette au ramassage des ordures une fois qu'elle est "terminée", même si des écouteurs d'événements lui sont associés. Je suppose donc que c’est la raison pour laquelle angular 2 guide officiel omet unsubscribe() et permet à GC de nettoyer les auditeurs.

Quant à votre deuxième exemple, cela dépend de la mise en oeuvre de params. À ce jour, le guide officiel angular ne montre plus le désabonnement de params. J'ai regardé dans src encore et ai trouvé que params est juste un BehaviorSubject . Etant donné qu'aucun écouteur d'événement ou minuterie n'a été utilisé et qu'aucune variable globale n'a été créée, il devrait être prudent d'omettre unsubscribe().

Le but de votre question est que vous appeliez toujours unsubscribe() comme 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 définit aucun minuteur ou ne fait rien d'autre. cela 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 sa unsubscribe(), qui est généralement la fonction renvoyée par le constructeur, vous avez de bonnes raisons d'envisager sérieusement l'appel de unsubscribe().

15
evenstar

La documentation officielle de Angular 2 fournit des explications sur le moment opportun pour se désabonner et le moment où il peut être ignoré en toute sécurité. Regardez ce lien:

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

Recherchez le paragraphe avec l'en-tête . Le parent et les enfants communiquent via un service , puis la zone bleue:

Notez que nous capturons l’abonnement et nous désabonnons lorsque l’AstronautComponent est détruit. C'est une étape de protection contre les fuites de mémoire. 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. Cela ne serait pas toujours vrai dans une application plus complexe.

Nous n'ajoutons pas cette protection au composant MissionControlComponent car, en tant que parent, il contrôle la durée de vie du service MissionService.

J'espère que ceci vous aide.

6
Cerny

Basé sur: tilisation de l'héritage de classe pour raccorder à Angular cycle de vie à 2 composants

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));
    }
}
5
JoG

Comme la solution de seangwright (Edit 3) semble très utile, j'ai également trouvé pénible d'intégrer cette fonctionnalité dans le composant de base et de laisser entendre aux autres coéquipiers du projet de ne pas oublier d'appeler super () sur ngOnDestroy pour l'activer.

Cette réponse permet de se libérer du super-appel et fait de "composant détruit $" un composant de base du composant.

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
Val

La réponse officielle de Edit # 3 (et ses variantes) fonctionne bien, mais ce qui me tient est la confusion de la logique commerciale autour de l'abonnement observable.

Voici une autre approche utilisant des wrappers.

Avertissement: code expérimental

Le fichier subscribeAndGuard.ts est utilisé pour créer une nouvelle extension Observable pour envelopper .subscribe() et à l'intérieur de celle-ci pour envelopper ngOnDestroy().
L'utilisation est identique à .subscribe(), à l'exception d'un premier paramètre supplémentaire faisant référence au 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, l'un avec le wrapper et l'autre sans. Le seul inconvénient est qu'il doit implémenter OnDestroy (avec un corps vide si désiré), sinon Angular ne sait pas appeler la version encapsulé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 pilote de démonstration est ici

Une note supplémentaire: Re Edit 3 - La solution 'officielle', cela 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;
  }
}
3
Richard Matsen

Suite à la réponse de @ seangwright , j'ai écrit une classe abstraite qui gère les abonnements "infinis" d'observables dans des composants:

import { OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { PartialObserver } from 'rxjs/Observer';

export abstract class InfiniteSubscriberComponent implements OnDestroy {
  private onDestroySource: Subject<any> = new Subject();

  constructor() {}

  subscribe(observable: Observable<any>): Subscription;

  subscribe(
    observable: Observable<any>,
    observer: PartialObserver<any>
  ): Subscription;

  subscribe(
    observable: Observable<any>,
    next?: (value: any) => void,
    error?: (error: any) => void,
    complete?: () => void
  ): Subscription;

  subscribe(observable: Observable<any>, ...subscribeArgs): Subscription {
    return observable
      .takeUntil(this.onDestroySource)
      .subscribe(...subscribeArgs);
  }

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

Pour l'utiliser, il suffit de l'étendre dans votre composant angular et d'appeler la méthode subscribe() comme suit:

this.subscribe(someObservable, data => doSomething());

Il accepte également l'erreur et complète les rappels comme d'habitude, un objet observateur, ou aucun rappel. N'oubliez pas d'appeler super.ngOnDestroy() si vous implémentez également cette méthode dans le composant enfant.

Trouvez ici une référence supplémentaire de Ben Lesh: RxJS: ne vous désabonnez pas .

3
Mau Muñoz

J'aime les deux dernières réponses, mais j'ai rencontré un problème si la sous-classe référencée "this" dans ngOnDestroy.

Je l'ai modifié pour être ceci, et il semble que cela résolve ce problème.

export abstract class BaseComponent implements OnDestroy {
    protected componentDestroyed$: Subject<boolean>;
    constructor() {
        this.componentDestroyed$ = new Subject<boolean>();
        let f = this.ngOnDestroy;
        this.ngOnDestroy = function()  {
            // without this I was getting an error if the subclass had
            // this.blah() in ngOnDestroy
            f.bind(this)();
            this.componentDestroyed$.next(true);
            this.componentDestroyed$.complete();
        };
    }
    /// placeholder of ngOnDestroy. no need to do super() call of extended class.
    ngOnDestroy() {}
}
2
Scott Williams

J'ai essayé la solution de seangwright (Edit 3)

Cela ne fonctionne pas pour Observable que celui créé par timer ou intervalle.

Cependant, je l'ai obtenu en utilisant une autre approche:

import { Component, OnDestroy, OnInit } from '@angular/core';
import 'rxjs/add/operator/takeUntil';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/Rx';

import { MyThingService } from '../my-thing.service';

@Component({
   selector: 'my-thing',
   templateUrl: './my-thing.component.html'
})
export class MyThingComponent implements OnDestroy, OnInit {
   private subscriptions: Array<Subscription> = [];

  constructor(
     private myThingService: MyThingService,
   ) { }

  ngOnInit() {
    const newSubs = this.myThingService.getThings()
        .subscribe(things => console.log(things));
    this.subscriptions.Push(newSubs);
  }

  ngOnDestroy() {
    for (const subs of this.subscriptions) {
      subs.unsubscribe();
   }
 }
}
2
Jeff Tham

Vous devez généralement vous désabonner lorsque les composants sont détruits, mais Angular le manipulera de plus en plus au fur et à mesure, par exemple dans la nouvelle version mineure d’Angular4, ils ont cette section pour la désinscription du routage:

Avez-vous besoin de vous désabonner?

Comme décrit dans ActivatedRoute: la section de guichet unique pour les informations sur les itinéraires de la page Routage et navigation, le routeur gère les éléments observables qu'il fournit et localise les abonnements. Les abonnements sont nettoyés lorsque le composant est détruit, ce qui vous protège des fuites de mémoire. Vous n'avez donc pas besoin de vous désabonner de la route paramMap Observable.

L'exemple ci-dessous est également un bon exemple tiré de Angular pour créer un composant et le détruire ensuite. Observez comment le composant implémente OnDestroy. Si vous avez besoin de onInit, vous pouvez également l'implémenter dans votre composant, comme implémente OnInit, OnDestroy

import { Component, Input, OnDestroy } from '@angular/core';  
import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs/Subscription';

@Component({
  selector: 'my-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        [disabled]="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})

export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;

  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }

  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }

  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}
1
Alireza

Si une désinscription est nécessaire, l'opérateur suivant pour la méthode de conduite observable peut être utilisé

import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { OnDestroy } from '@angular/core';

export const takeUntilDestroyed = (componentInstance: OnDestroy) => <T>(observable: Observable<T>) => {
  const subjectPropertyName = '__takeUntilDestroySubject__';
  const originalOnDestroy = componentInstance.ngOnDestroy;
  const componentSubject = componentInstance[subjectPropertyName] as Subject<any> || new Subject();

  componentInstance.ngOnDestroy = (...args) => {
    originalOnDestroy.apply(componentInstance, args);
    componentSubject.next(true);
    componentSubject.complete();
  };

  return observable.pipe(takeUntil<T>(componentSubject));
};

il peut être utilisé comme ceci:

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

@Component({ template: '<div></div>' })
export class SomeComponent implements OnInit, OnDestroy {

  ngOnInit(): void {
    const observable = Observable.create(observer => {
      observer.next('Hello');
    });

    observable
      .pipe(takeUntilDestroyed(this))
      .subscribe(val => console.log(val));
  }

  ngOnDestroy(): void {
  }
}

L'opérateur encapsule la méthode ngOnDestroy du composant.

Important: l'opérateur doit être le dernier dans une conduite observable.

1
Oleg Polezky

Un autre bref ajout aux situations susmentionnées est le suivant:

  • Toujours vous désabonner, lorsque de nouvelles valeurs dans le flux souscrit ne sont plus nécessaires ou n'ont pas d'importance, le nombre de déclencheurs sera considérablement réduit et les performances augmenteront dans quelques cas. Des cas tels que des composants pour lesquels les données/événements souscrits n'existent plus ou lorsqu'un nouvel abonnement à un nouveau flux est requis (actualisation, etc.) constituent un bon exemple de désinscription.
1
kg11

dans l'application SPA à ngOnDestroy fonction (cycle de vie angulaire) Pour chaque s'abonner vous devez vous désabonner le. avantage => pour éviter que l'état ne devienne trop lourd.

par exemple: dans composant1:

import {UserService} from './user.service';

private user = {name: 'test', id: 1}

constructor(public userService: UserService) {
    this.userService.onUserChange.next(this.user);
}

en service:

import {BehaviorSubject} from 'rxjs/BehaviorSubject';

public onUserChange: BehaviorSubject<any> = new BehaviorSubject({});

dans le composant2:

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
mojtaba ramezani

Pour la gestion de l'abonnement, j'utilise une classe "Unsubscriber".

Voici la classe Unsubscriber.

export class Unsubscriber implements OnDestroy {
  private subscriptions: Subscription[] = [];

  addSubscription(subscription: Subscription | Subscription[]) {
    if (Array.isArray(subscription)) {
      this.subscriptions.Push(...subscription);
    } else {
      this.subscriptions.Push(subscription);
    }
  }

  unsubscribe() {
    this.subscriptions
      .filter(subscription => subscription)
      .forEach(subscription => {
        subscription.unsubscribe();
      });
  }

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

Et vous pouvez utiliser cette classe dans n’importe quel composant/service/effet, etc.

Exemple:

class SampleComponent extends Unsubscriber {
    constructor () {
        super();
    }

    this.addSubscription(subscription);
}
0
Pratiyush