Angular / RxJs ¿Cuándo debo darme de baja de `Subscription`

720

¿Cuándo debo almacenar las Subscriptioninstancias e invocar unsubscribe()durante el ciclo de vida de NgOnDestroy y cuándo puedo simplemente ignorarlas?

Guardar todas las suscripciones introduce mucho desorden en el código del componente.

La Guía del cliente HTTP ignora las suscripciones como esta:

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

Al mismo tiempo, la Guía de ruta y navegación dice que:

Finalmente, navegaremos a otro lugar. El enrutador eliminará este componente del DOM y lo destruirá. Necesitamos limpiar después de nosotros mismos antes de que eso suceda. Específicamente, debemos cancelar la suscripción antes de que Angular destruya el componente. De lo contrario, podría crear una pérdida de memoria.

Nos damos de baja de nuestro Observableen el ngOnDestroymétodo.

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();
}
Sergey Tihon
fuente
21
Supongo Subscriptionque http-requestsse puede ignorar, ya que solo llaman onNextuna vez y luego llaman onComplete. En su Routerlugar, llama onNextrepetidamente y puede que nunca llame onComplete(no estoy seguro de eso ...). Lo mismo ocurre con Observables de Events. Así que supongo que deberían ser unsubscribed.
Springrbua
1
@ gt6707a La secuencia se completa (o no se completa) independientemente de cualquier observación de esa finalización. Las devoluciones de llamada (el observador) proporcionadas a la función de suscripción no determinan si se asignan recursos. Es la llamada a subscribesí mismo la que potencialmente asigna recursos aguas arriba.
seangwright

Respuestas:

981

--- Edición 4 - Recursos adicionales (2018/09/01)

En un episodio reciente de Adventures in Angular, Ben Lesh y Ward Bell discuten los problemas en torno a cómo / cuándo darse de baja en un componente. La discusión comienza aproximadamente a las 1:05:30.

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

En respuesta, Ben menciona que hay discusiones en este momento para permitir que los Observables se conecten a los eventos del ciclo de vida del componente Angular y Ward sugiere un Observable de los eventos del ciclo de vida al que un componente podría suscribirse para saber cuándo completar los Observables mantenidos como estado interno del componente.

Dicho esto, en su mayoría necesitamos soluciones ahora, así que aquí hay algunos otros recursos.

  1. Una recomendación para el takeUntil()patrón del miembro del equipo central de RxJ, Nicholas Jamieson, y una regla tslint para ayudar a hacerla cumplir. https://ncjamieson.com/avoiding-takeuntil-leaks/

  2. Paquete npm ligero que expone un operador Observable que toma una instancia de componente ( this) como parámetro y se da de baja automáticamente durante ngOnDestroy. https://github.com/NetanelBasal/ngx-take-until-destroy

  3. Otra variación de lo anterior con una ergonomía ligeramente mejor si no está haciendo AOT builds (pero todos deberíamos estar haciendo AOT ahora). https://github.com/smnbbrv/ngx-rx-collector

  4. Directiva personalizada *ngSubscribeque funciona como una tubería asíncrona pero crea una vista incrustada en su plantilla para que pueda consultar el valor 'sin envolver' en toda su plantilla. https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

Menciono en un comentario al blog de Nicholas que el uso excesivo de takeUntil()podría ser una señal de que su componente está tratando de hacer demasiado y que se debe considerar la separación de sus componentes existentes en componentes de Presentación y Característica . Luego puede hacer el Observable desde el componente Característica en uno del componente Presentacional, lo que significa que no se necesitan suscripciones en ningún lado. Lea más sobre este enfoque aquí| asyncInput

--- Edición 3 - La solución 'oficial' (2017/04/09)

Hablé con Ward Bell sobre esta pregunta en NGConf (incluso le mostré esta respuesta y dijo que era correcta), pero me dijo que el equipo de documentación de Angular tenía una solución para esta pregunta que no se ha publicado (aunque están trabajando para lograr que sea aprobada). ) También me dijo que podía actualizar mi respuesta SO con la próxima recomendación oficial.

La solución que todos deberíamos usar en el futuro es agregar un private ngUnsubscribe = new Subject();campo a todos los componentes que tienen .subscribe()llamadas a Observables dentro de su código de clase.

Luego llamamos this.ngUnsubscribe.next(); this.ngUnsubscribe.complete();a nuestros ngOnDestroy()métodos.

La salsa secreta (como ya señaló @metamaker ) es llamar takeUntil(this.ngUnsubscribe)antes de cada una de nuestras .subscribe()llamadas, lo que garantizará que todas las suscripciones se limpien cuando se destruya el componente.

Ejemplo:

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

Nota: Es importante agregar el takeUntiloperador como el último para evitar fugas con observables intermedios en la cadena del operador.

--- Edición 2 (28/12/2016)

Fuente 5

El tutorial Angular, el capítulo Enrutamiento ahora establece lo siguiente: "El enrutador administra los observables que proporciona y localiza las suscripciones. Las suscripciones se limpian cuando se destruye el componente, protegiéndolo contra pérdidas de memoria, por lo que no necesitamos cancelar la suscripción de los parámetros de ruta observables ". - Mark Rajcok

Aquí hay una discusión sobre los problemas de Github para los documentos angulares con respecto a los observadores del enrutador, donde Ward Bell menciona que se está aclarando todo esto.

--- Editar 1

Fuente 4

En este video de NgEurope, Rob Wormald también dice que no es necesario darse de baja de Router Observables. También menciona el httpservicio y ActivatedRoute.paramsen este video de noviembre de 2016 .

--- Respuesta original

TLDR:

Para esta pregunta hay (2) tipos de Observables- valor finito y valor infinito .

http Observablesproducir valores finitos (1) y algo así como un DOM event listener Observablesproduce valores infinitos .

Si llama manualmente subscribe(sin usar tubería asíncrona), entonces unsubscribedesde infinito Observables .

No te preocupes por los finitos , los cuidaremosRxJs .

Fuente 1

Rastreé una respuesta de Rob Wormald en Angular's Gitter aquí .

Él afirma (me reorganicé para mayor claridad y el énfasis es mío)

si es una secuencia de un solo valor (como una solicitud http), la limpieza manual es innecesaria (suponiendo que se suscriba manualmente en el controlador)

debería decir "si es una secuencia que se completa " (de las cuales las secuencias de valor único, a la http, son una)

si es una secuencia infinita , debe darse de baja, lo que la tubería asíncrona hace por usted

También menciona en este video de YouTube en Observables que they clean up after themselves... en el contexto de Observables que complete(como Promesas, que siempre se completan porque siempre producen 1 valor y finalizan), nunca nos preocupamos por cancelar la suscripción a Promesas para asegurarnos de que limpien el xhrevento oyentes, ¿verdad?

Fuente 2

También en la guía Rangle de Angular 2 se lee

En la mayoría de los casos, no necesitaremos llamar explícitamente al método de cancelación de la suscripción a menos que queramos cancelar antes o que nuestro Observable tenga una vida útil más larga que nuestra suscripción. El comportamiento predeterminado de los operadores Observables es deshacerse de la suscripción tan pronto como se publiquen los mensajes .complete () o .error (). Tenga en cuenta que RxJS fue diseñado para ser utilizado en una forma de "disparar y olvidar" la mayor parte del tiempo.

¿Cuándo se aplica la frase our Observable has a longer lifespan than our subscription?

Se aplica cuando se crea una suscripción dentro de un componente que se destruye antes (o no 'mucho' antes) de que se Observablecomplete.

Leí esto como significado si nos suscribimos a una httpsolicitud o un observable que emite 10 valores y nuestro componente se destruye antes de que esa httpsolicitud regrese o se hayan emitido los 10 valores, ¡todavía estamos bien!

Cuando la solicitud regrese o finalmente se emita el décimo valor, Observablese completará y todos los recursos se limpiarán.

Fuente 3

Si miramos este ejemplo de la misma guía Rangle, podemos ver que el Subscriptionto route.paramsrequiere un unsubscribe()porque no sabemos cuándo paramsdejarán de cambiar (emitiendo nuevos valores).

El componente podría destruirse navegando, en cuyo caso los parámetros de ruta probablemente seguirán cambiando (técnicamente podrían cambiar hasta que finalice la aplicación) y los recursos asignados en la suscripción aún se asignarían porque no ha habido un completion.

seangwright
fuente
16
Llamar complete()por sí solo no parece limpiar las suscripciones. Sin embargo, al llamar next()y luego lo complete()hace, creo que takeUntil()solo se detiene cuando se produce un valor, no cuando finaliza la secuencia.
Firefly
3
@seangwright Una prueba rápida con un miembro de tipo Subjectdentro de un componente y que se activa ngIfpara activar ngOnInity ngOnDestroymuestra que el sujeto y sus suscripciones nunca se completarán o eliminarán (conectó un finallyoperador a la suscripción). Debo llamar Subject.complete()en ngOnDestroy, por lo que las suscripciones pueden limpiar ellos mismos.
Lars
44
Tu --- Edit 3 es muy perspicaz, ¡gracias! Solo tengo una pregunta de seguimiento: si takeUnitlutilizamos el enfoque, ¿nunca tendremos que darnos de baja manualmente de ningún observable? ¿Es ese el caso? Por otra parte, ¿por qué necesitamos a la llamada next()en el ngOnDestroy, ¿por qué no llamamos complete()?
uglycode
77
@seangwright Eso es decepcionante; El repetitivo adicional es molesto.
spongessuck
44
Editar 3 discutido en el contexto de los eventos en medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87
HankCa
91

No necesita tener muchas suscripciones y darse de baja manualmente. Use el combo Asunto y takeUntil para manejar suscripciones como un jefe:

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

El enfoque alternativo , que fue propuesto por @acumartini en los comentarios , utiliza takeWhile en lugar de takeUntil . Puede preferirlo, pero tenga en cuenta que de esta manera su ejecución Observable no se cancelará en ngDestroy de su componente (por ejemplo, cuando realiza cálculos que requieren mucho tiempo o espera datos del servidor). El método, que se basa en takeUntil , no tiene este inconveniente y conduce a la cancelación inmediata de la solicitud. Gracias a @AlexChe por la explicación detallada en los comentarios .

Entonces aquí está el código:

@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
  }
}
metamaker
fuente
2
Si solo usa un bool para mantener el estado, ¿cómo hacer que "takeUntil" funcione como se espera?
Val
66
Creo que hay una diferencia significativa entre usar takeUntily takeWhile. El primero se da de baja de la fuente observable inmediatamente cuando se dispara, mientras que el último se da de baja tan pronto como la fuente observable produce el siguiente valor. Si producir un valor por la fuente observable es una operación que consume recursos, elegir entre los dos puede ir más allá de la preferencia de estilo. Vea el golpe
Alex Che
2
¡@AlexChe gracias por proporcionar un golpe interesante! Este es un punto muy válido para el uso general de takeUntilvs takeWhile, sin embargo, no para nuestro caso específico. Cuando necesitamos cancelar la suscripción de oyentes en la destrucción de componentes , solo estamos verificando el valor booleano como () => aliveen takeWhile, por lo que no se utilizan las operaciones que consumen tiempo / memoria y la diferencia se trata básicamente del estilo (ofc, para este caso específico).
metamaker
2
@metamaker Digamos, en nuestro componente nos suscribimos a una Observable, que extrae internamente algunas criptomonedas y dispara un nextevento por cada moneda extraída, y extraer una de esas monedas lleva un día. Con takeUntilnos daremos de baja de la minería de origen Observableinmediatamente una vez que ngOnDestroyse llama durante la destrucción de nuestro componente. Por lo tanto, la Observablefunción de minería puede cancelar su operación inmediatamente durante este proceso.
Alex Che
2
OTOH, si usamos takeWhile, en el ngOnDestorysolo establecemos la variable booleana. Pero la Observablefunción de minería aún podría funcionar hasta por un día, y solo entonces, durante su nextllamada, se dará cuenta de que no hay suscripciones activas y debe cancelarse.
Alex Che
76

La clase de suscripción tiene una característica interesante:

Representa un recurso desechable, como la ejecución de un Observable. Una suscripción tiene un método importante, darse de baja, que no requiere argumentos y simplemente elimina el recurso que posee la suscripción.
Además, las suscripciones se pueden agrupar mediante el método add (), que adjuntará una Suscripción secundaria a la Suscripción actual. Cuando una suscripción se cancela, todos sus hijos (y sus nietos) también se cancelarán.

Puede crear un objeto de Suscripción agregado que agrupe todas sus suscripciones. Para ello, cree una Suscripción vacía y agregue suscripciones mediante su add()método. Cuando su componente se destruye, solo necesita cancelar la suscripción agregada.

@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();
  }
}
Steven Liekens
fuente
1
Estoy usando este enfoque. ¿Se pregunta si esto es mejor que usar el enfoque con takeUntil (), como en la respuesta aceptada ... inconvenientes?
Manuel Di Iorio
1
No hay inconvenientes que yo sepa. No creo que esto sea mejor, solo diferente.
Steven Liekens
3
Consulte medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 para obtener más información sobre el takeUntilenfoque oficial frente a este enfoque de recopilación de suscripciones y llamadas unsubscribe. (Este enfoque me parece mucho más limpio.)
Josh Kelley
44
Un pequeño beneficio de esta respuesta: no tiene que verificar si this.subscriptionses nulo
user2023861
2
Solo evite encadenar métodos de agregar, como sub = subsciption.add(..).add(..)porque en muchos casos produce resultados inesperados github.com/ReactiveX/rxjs/issues/2769#issuecomment-345636477
Evgeniy Generalov
32

Algunas de las mejores prácticas con respecto a las bajas de observables dentro de los componentes angulares:

Una cita de Routing & Navigation

Al suscribirse a un observable en un componente, casi siempre se arregla para darse de baja cuando se destruye el componente.

Hay algunos observables excepcionales donde esto no es necesario. Los observables de ActivatedRoute se encuentran entre las excepciones.

La ruta activada y sus observables están aislados del enrutador. El enrutador destruye un componente enrutado cuando ya no es necesario y la ruta activada inyectada muere con él.

Siéntase libre de darse de baja de todos modos. Es inofensivo y nunca es una mala práctica.

Y en respuesta a los siguientes enlaces:

Recopilé algunas de las mejores prácticas con respecto a las bajas de observables dentro de los componentes de Angular para compartir con ustedes:

  • httpla cancelación de la suscripción observable es condicional y deberíamos considerar los efectos de la 'devolución de llamada de suscripción' que se ejecuta después de que el componente se destruya caso por caso. Sabemos que angular cancela la suscripción y limpia lo httpobservable en sí (1) , (2) . Si bien esto es cierto desde la perspectiva de los recursos, solo cuenta la mitad de la historia. Digamos que estamos hablando de llamar directamente httpdesde un componente, y la httprespuesta tardó más de lo necesario, por lo que el usuario cerró el componente. lossubscribe()aún se llamará al controlador incluso si el componente está cerrado y destruido. Esto puede tener efectos secundarios no deseados y, en el peor de los casos, dejar roto el estado de la aplicación. También puede causar excepciones si el código en la devolución de llamada intenta llamar a algo que acaba de eliminarse. Sin embargo, al mismo tiempo ocasionalmente se desean. Por ejemplo, supongamos que está creando un cliente de correo electrónico y activa un sonido cuando el correo electrónico termina de enviarse; bueno, aún así desearía que eso ocurra incluso si el componente está cerrado ( 8 ).
  • No es necesario darse de baja de los observables que completan o error. Sin embargo, no hay daño al hacerlo (7) .
  • Use AsyncPipetanto como sea posible porque se da de baja automáticamente del observable en la destrucción de componentes.
  • Anule la suscripción de los ActivatedRouteobservables como route.paramssi estuvieran suscritos dentro de un componente anidado (agregado dentro de tpl con el selector de componentes) o dinámico, ya que pueden suscribirse muchas veces mientras exista el componente padre / host. No es necesario darse de baja de ellos en otros escenarios como se menciona en la cita anterior de los Routing & Navigationdocumentos.
  • Anule la suscripción de observables globales compartidos entre componentes que están expuestos a través de un servicio Angular, por ejemplo, ya que pueden suscribirse varias veces siempre que el componente se inicialice.
  • No es necesario darse de baja de los observables internos de un servicio con ámbito de aplicación, ya que este servicio nunca se destruye, a menos que se destruya toda su aplicación, no hay ninguna razón real para darse de baja y no hay posibilidad de pérdidas de memoria. (6) .

    Nota: Con respecto a los servicios con alcance, es decir, los proveedores de componentes, se destruyen cuando se destruye el componente. En este caso, si nos suscribimos a cualquier observable dentro de este proveedor, deberíamos considerar darnos de baja mediante el OnDestroyenlace del ciclo de vida que se llamará cuando se destruya el servicio, según los documentos.
  • Use una técnica abstracta para evitar cualquier desorden de código que pueda resultar de las bajas. Puede administrar sus suscripciones con takeUntil (3) o puede usar este npm paquete mencionado en (4) La forma más fácil de darse de baja de Observables en Angular .
  • Siempre darse de baja de FormGroupobservables como form.valueChangesyform.statusChanges
  • Siempre anule la suscripción de observables de Renderer2servicio comorenderer2.listen
  • Anule la suscripción de todos los demás observables como un paso de protección de pérdida de memoria hasta que Angular Docs nos diga explícitamente qué observables son innecesarios para anular (consulte el problema: (5) Documentación para la cancelación de la suscripción de RxJS (Abrir) ).
  • Bonificación: siempre use las formas angulares para vincular eventos HostListenercomo angular, se preocupa bien por eliminar los oyentes de eventos si es necesario y evita cualquier pérdida potencial de memoria debido a enlaces de eventos.

Un buen consejo final : si no sabe si un observable se cancelará / completará automáticamente o no, agregue una completedevolución de llamada subscribe(...)y verifique si se llama cuando se destruye el componente.

Mouneer
fuente
La respuesta para el número 6 no es del todo correcta. Los servicios se destruyen y ngOnDestroyse llama cuando el servicio se proporciona en un nivel diferente al nivel raíz, por ejemplo, se proporciona explícitamente en un componente que luego se elimina. En estos casos, debe darse de baja de los servicios observables internos
Drenai
@Drenai, gracias por tu comentario y cortésmente no estoy de acuerdo. Si se destruye un componente, el componente, el servicio y el observable serán todos GC y la cancelación de la suscripción será inútil en este caso, a menos que mantenga una referencia para el observable en cualquier lugar lejos del componente (lo que no es lógico filtrar los estados del componente a nivel mundial a pesar del alcance del servicio al componente)
Mouneer
Si el servicio que se está destruyendo tiene una suscripción a un observable que pertenece a otro servicio más arriba en la jerarquía DI, entonces GC no ocurrirá. Evite este escenario al darse de baja ngOnDestroy, que siempre se llama cuando se destruyen los servicios github.com/angular/angular/commit/…
Drenai
1
@Drenai, verifica la respuesta actualizada.
Mouneer
3
@Tim Primero que nada, Feel free to unsubscribe anyway. It is harmless and never a bad practice.y con respecto a tu pregunta, depende. Si el componente secundario se inicia varias veces (por ejemplo, se agrega dentro ngIfo se carga dinámicamente), debe cancelar la suscripción para evitar agregar múltiples suscripciones al mismo observador. De lo contrario no es necesario. Pero prefiero cancelar la suscripción dentro del componente secundario, ya que esto lo hace más reutilizable y aislado de cómo podría usarse.
Mouneer
18

Depende. Si, al llamar someObservable.subscribe(), comienza a retener algunos recursos que deben liberarse manualmente cuando finaliza el ciclo de vida de su componente, debe llamar theSubscription.unsubscribe()para evitar pérdidas de memoria.

Echemos un vistazo más de cerca a sus ejemplos:

getHero()devuelve el resultado de http.get(). Si observa el código fuente angular 2 , http.get()crea dos oyentes de eventos:

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

y al llamar unsubscribe(), puede cancelar la solicitud y los oyentes:

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

Tenga en cuenta que _xhres específico de la plataforma, pero creo que es seguro asumir que es un XMLHttpRequest()caso en su caso.

Normalmente, esta es evidencia suficiente para justificar un manual unsubscribe() llamada . Pero de acuerdo con esta especificación WHATWG , XMLHttpRequest()está sujeto a la recolección de basura una vez que está "hecho", incluso si hay oyentes de eventos adjuntos. Así que supongo que es por eso que la guía oficial angular 2 omite unsubscribe()y permite que GC limpie a los oyentes.

En cuanto a su segundo ejemplo, depende de la implementación de params. A partir de hoy, la guía oficial angular ya no muestra la cancelación de la suscripción params. Miré en src nuevamente y descubrí que paramses solo un BehaviorSubject . Como no se utilizaron oyentes ni temporizadores de eventos, y no se crearon variables globales, debería ser seguro omitirlos unsubscribe().

La conclusión de su pregunta es que siempre debe actuar unsubscribe()como protección contra la pérdida de memoria, a menos que esté seguro de que la ejecución del observable no crea variables globales, agrega oyentes de eventos, establece temporizadores o hace cualquier otra cosa que provoque pérdidas de memoria. .

En caso de duda, investigue la implementación de ese observable. Si el observable ha escrito alguna lógica de limpieza en él unsubscribe(), que generalmente es la función que devuelve el constructor, entonces tiene buenas razones para considerar seriamente llamar unsubscribe().

Chuanqi Sun
fuente
6

La documentación oficial de Angular 2 proporciona una explicación de cuándo darse de baja y cuándo puede ignorarse de manera segura. Echa un vistazo a este enlace:

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

Busque el párrafo con el título Los padres y los niños se comunican a través de un servicio y luego el cuadro azul:

Tenga en cuenta que capturamos la suscripción y cancelamos la suscripción cuando se destruye el Componente Astronauta. Este es un paso de protección de pérdida de memoria. No hay ningún riesgo real en esta aplicación porque la vida útil de un componente de astronauta es la misma que la vida útil de la aplicación en sí. Eso no siempre sería cierto en una aplicación más compleja.

No agregamos este protector al Componente de control de misión porque, como padre, controla la vida útil del Servicio de misión.

Espero que esto te ayude.

Cerny
fuente
3
como componente, nunca se sabe si eres un niño o no. por lo tanto, siempre debe darse de baja de las suscripciones como práctica recomendada.
SeriousM
1
El punto sobre MissionControlComponent no es realmente si es un padre o no, es que el componente en sí mismo proporciona el servicio. Cuando MissionControl se destruye, también lo hace el servicio y cualquier referencia a la instancia del servicio, por lo que no hay posibilidad de una fuga.
termina el
6

Basado en: Uso de la herencia de clase para conectar al ciclo de vida del componente Angular 2

Otro enfoque genérico:

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
  }

}

Y use :

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

Empujoncito
fuente
1
Esto NO funciona correctamente. Tenga cuidado al usar esta solución. Estás perdiendo una this.componentDestroyed$.next()llamada como la solución aceptada por
Sean
4

La respuesta oficial de Edit # 3 (y variaciones) funciona bien, pero lo que me sorprende es el "enturbiamiento" de la lógica de negocios en torno a la suscripción observable.

Aquí hay otro enfoque usando envoltorios.

Warining: código experimental

El archivo subscribeAndGuard.ts se usa para crear una nueva extensión Observable para envolver .subscribe()y dentro de ella para envolver ngOnDestroy().
El uso es el mismo que .subscribe(), excepto por un primer parámetro adicional que hace referencia al componente.

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

Aquí hay un componente con dos suscripciones, una con el contenedor y otra sin él. La única advertencia es que debe implementar OnDestroy (con el cuerpo vacío si lo desea), de lo contrario, Angular no sabe llamar a la versión envuelta.

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 demo plunker está aquí

Una nota adicional: Re Edit 3 - La solución 'oficial', esto se puede simplificar usando takeWhile () en lugar de takeUntil () antes de las suscripciones, y un simple booleano en lugar de otro Observable en 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;
  }
}
Richard Matsen
fuente
3

Dado que la solución de seangwright (Edición 3) parece ser muy útil, también encontré un problema incluir esta característica en el componente base, e insinuar a otros compañeros de equipo del proyecto que recuerden llamar a super () en ngOnDestroy para activar esta característica.

Esta respuesta proporciona una forma de liberarse de la súper llamada y hacer que "componentDestroyed $" sea un núcleo del componente 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() {}
}

Y luego puede usar esta función libremente, por ejemplo:

@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');
    }

}
Val
fuente
3

Siguiendo la respuesta de @seangwright , he escrito una clase abstracta que maneja las suscripciones "infinitas" de observables en componentes:

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

Para usarlo, simplemente extiéndalo en su componente angular y llame al subscribe()método de la siguiente manera:

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

También acepta el error y completa las devoluciones de llamada como de costumbre, un objeto de observación, o no devoluciones de llamada en absoluto. Recuerde llamar super.ngOnDestroy()si también está implementando ese método en el componente secundario.

Encuentre aquí una referencia adicional de Ben Lesh: RxJS: Don't Unsubscribe .

Mau Muñoz
fuente
2

Probé la solución de seangwright (Edición 3)

Eso no funciona para Observable creado por temporizador o intervalo.

Sin embargo, lo hice funcionar usando otro enfoque:

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();
   }
 }
}
Jeff Tham
fuente
2

Me gustan las dos últimas respuestas, pero experimenté un problema si la subclase a la que se hace referencia "this"enngOnDestroy .

Lo modifiqué para que sea esto, y parece que resolvió ese problema.

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() {}
}
Scott Williams
fuente
necesita usar la función de flecha para vincular el 'esto':this.ngOnDestroy = () => { f.bind(this)(); this.componentDestroyed$.complete(); };
Damsorian
2

En caso de que sea necesario darse de baja, se puede utilizar el siguiente operador para el método de tubería observable

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

se puede usar así:

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 {
  }
}

El operador ajusta el método ngOnDestroy del componente.

Importante: el operador debe ser el último en tubería observable.

Oleg Polezky
fuente
Esto funcionó muy bien, sin embargo, actualizar a angular 9 parece matarlo. Alguien sabe por qué?
ymerej
1

Por lo general, debe darse de baja cuando los componentes se destruyen, pero Angular lo manejará cada vez más a medida que avanzamos, por ejemplo, en una nueva versión menor de Angular4, tienen esta sección para cancelar la suscripción de enrutamiento:

¿Necesitas darte de baja?

Como se describe en la sección ActivatedRoute: la ventanilla única para la información de ruta de la página Enrutamiento y navegación, el enrutador administra los observables que proporciona y localiza las suscripciones. Las suscripciones se limpian cuando se destruye el componente, lo que protege contra pérdidas de memoria, por lo que no es necesario darse de baja de la ruta paramMap Observable.

Además, el siguiente ejemplo es un buen ejemplo de Angular para crear un componente y destruirlo después, observe cómo el componente implementa OnDestroy, si necesita onInit, también puede implementarlo en su componente, como implementos 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();
  }
}
Alireza
fuente
3
Confuso. Que estas diciendo aquí? Usted (documentos / notas recientes de Angular) parece decir que Angular se encarga de ello y luego confirma que darse de baja es un buen patrón. Gracias.
jamie
1

Otra pequeña adición a las situaciones mencionadas anteriormente es:

  • Siempre anule la suscripción, cuando los nuevos valores en la secuencia suscrita ya no sean necesarios o no importen, resultará en una menor cantidad de disparadores y aumentará el rendimiento en algunos casos. Casos como componentes donde los datos / eventos suscritos ya no existen o se requiere una nueva suscripción a una transmisión completamente nueva (actualización, etc.) es un buen ejemplo para cancelar la suscripción.
Krishna Ganeriwal
fuente
0

en la aplicación SPA en la función ngOnDestroy (angular lifeCycle) Para cada suscripción , debe cancelar la suscripción . ventaja => para evitar que el estado se vuelva demasiado pesado.

por ejemplo: en el componente 1:

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

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

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

en servicio:

import {BehaviorSubject} from 'rxjs/BehaviorSubject';

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

en el componente2:

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();
}
mojtaba ramezani
fuente
0

Para manejar la suscripción, uso una clase de "Cancelar suscripción".

Aquí está la clase para darse de baja.

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

Y puede usar esta clase en cualquier componente / Servicio / Efecto, etc.

Ejemplo:

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

    this.addSubscription(subscription);
}
Pratiyush
fuente
0

Puede usar la última Subscriptionclase para darse de baja del Observable con un código no tan desordenado.

Podemos hacer esto, normal variablepero estará override the last subscriptionen cada nueva suscripción, así que evite eso, y este enfoque es muy útil cuando se trata de un mayor número de Obseravables y tipo de Obeservables como BehavoiurSubjectySubject

Suscripción

Representa un recurso desechable, como la ejecución de un Observable. Una suscripción tiene un método importante, darse de baja, que no requiere argumentos y solo elimina el recurso que posee la suscripción.

puedes usar esto de dos maneras,

  • puede enviar directamente la suscripción a la matriz de suscripción

     subscriptions:Subscription[] = [];
    
     ngOnInit(): void {
    
       this.subscription.push(this.dataService.getMessageTracker().subscribe((param: any) => {
                //...  
       }));
    
       this.subscription.push(this.dataService.getFileTracker().subscribe((param: any) => {
            //...
        }));
     }
    
     ngOnDestroy(){
        // prevent memory leak when component destroyed
        this.subscriptions.forEach(s => s.unsubscribe());
      }
    
  • uso add()deSubscription

    subscriptions = new Subscription();
    
    this.subscriptions.add(subscribeOne);
    this.subscriptions.add(subscribeTwo);
    
    ngOnDestroy() {
      this.subscriptions.unsubscribe();
    }
    

A Subscriptionpuede retener suscripciones secundarias y cancelarlas de forma segura. Este método maneja posibles errores (por ejemplo, si alguna suscripción secundaria es nula).

Espero que esto ayude.. :)

ganesh045
fuente
0

El paquete SubSink, una solución fácil y consistente para darse de baja

Como nadie más lo ha mencionado, quiero recomendar el paquete Subsink creado por Ward Bell: https://github.com/wardbell/subsink#readme .

Lo he estado usando en un proyecto donde todos somos desarrolladores y todos lo estamos usando. Ayuda mucho tener una manera consistente que funcione en cada situación.

SnorreDan
fuente
0

Para observables que se completan directamente después de emitir el resultado, como AsyncSubjectpor ejemplo observables de solicitudes http y tales, no es necesario darse de baja. No está de más pedir unsubscribe()esos, pero si el closedmétodo de cancelación de suscripción es observable , simplemente no hará nada :

if (this.closed) {
  return;
}

Cuando tiene observables de larga duración que emiten varios valores a lo largo del tiempo (como por ejemplo a BehaviorSubjecto a ReplaySubject), debe cancelar la suscripción para evitar pérdidas de memoria.

Puede crear fácilmente un observable que se complete directamente después de emitir un resultado de dichos observables de larga vida utilizando un operador de tubería. En algunas respuestas aquí take(1)se menciona la tubería. Pero prefiero la first()pipa . La diferencia take(1)es que:

entregar una EmptyErrordevolución de llamada de error del Observador si el Observable se completa antes de que se envíe cualquier próxima notificación.

Otra ventaja de la primera tubería es que puede pasar un predicado que lo ayudará a devolver el primer valor que satisfaga ciertos criterios:

const predicate = (result: any) => { 
  // check value and return true if it is the result that satisfies your needs
  return true;
}
observable.pipe(first(predicate)).subscribe(observer);

Primero se completará directamente después de emitir el primer valor (o al pasar un argumento de función al primer valor que satisfaga su predicado), por lo que no es necesario darse de baja.

A veces no está seguro de si tiene un observable de larga vida o no. No digo que sea una buena práctica, pero siempre puede agregar la firsttubería solo para asegurarse de que no necesitará darse de baja manualmente. Agregar una firsttubería adicional en un observable que emitirá solo un valor no hace daño.

Durante el desarrollo, puede usar la singletubería que fallará si la fuente observable emite varios eventos. Esto puede ayudarlo a explorar el tipo de observable y si es necesario darse de baja o no.

observable.pipe(single()).subscribe(observer);

El firsty singleparece muy similar, ambas tuberías pueden tomar un predicado opcional, pero las diferencias son importantes y están bien resumidas en esta respuesta de stackoverflow aquí :

primero

Se emitirá tan pronto como aparezca el primer elemento. Se completará justo después de eso.

Soltero

Fallará si la fuente observable emite varios eventos.


Tenga en cuenta que intenté ser lo más preciso y completo posible en mi respuesta con referencias a la documentación oficial, pero comente si falta algo importante ...

Marchitar
fuente
-1

--- Actualización de Angular 9 y Rxjs 6 Solution

  1. Uso unsubscribeen el ngDestroyciclo de vida del componente angular
class SampleComponent implements OnInit, OnDestroy {
  private subscriptions: Subscription;
  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$.subscribe( ... );
  }

  ngOnDestroy() {
    this.subscriptions.unsubscribe();
  }
}
  1. Utilizando takeUntilen Rxjs
class SampleComponent implements OnInit, OnDestroy {
  private unsubscribe$: new Subject<void>;
  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$
    .pipe(takeUntil(this.unsubscribe$))
    .subscribe( ... );
  }

  ngOnDestroy() {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}
  1. para alguna acción a la que llame en ngOnInitese momento solo ocurre una vez cuando componente init.
class SampleComponent implements OnInit {

  private sampleObservable$: Observable<any>;

  constructor () {}

  ngOnInit(){
    this.subscriptions = this.sampleObservable$
    .pipe(take(1))
    .subscribe( ... );
  }
}

También tenemos asyncpipa. Pero, este uso en la plantilla (no en el componente angular).

Hoang Tran Son
fuente
Tu primer ejemplo está incompleto.
Paul-Sebastian Manole