Angular / RxJs Quando devo cancelar a assinatura de `Subscription`

720

Quando devo armazenar as Subscriptioninstâncias e chamar unsubscribe()durante o ciclo de vida do NgOnDestroy e quando posso simplesmente ignorá-las?

Salvar todas as assinaturas introduz muita confusão no código do componente.

O Guia do Cliente HTTP ignora assinaturas como esta:

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

Ao mesmo tempo, o Guia de rotas e navegação diz que:

Eventualmente, navegaremos para outro lugar. O roteador removerá esse componente do DOM e o destruirá. Precisamos nos limpar antes que isso aconteça. Especificamente, devemos cancelar a inscrição antes que o Angular destrua o componente. Não fazer isso pode criar um vazamento de memória.

Nós desinscrever de nossa Observableno 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
fonte
21
Eu acho Subscriptionque http-requestspode ser ignorado, pois eles ligam apenas onNextuma vez e depois ligam onComplete. Em Routervez disso, chama onNextrepetidamente e pode nunca ligar onComplete(não tem certeza disso ...). O mesmo vale para Observables de Events. Então acho que deveriam ser unsubscribed.
Springrbua
1
@ gt6707a A transmissão é concluída (ou não é concluída) independentemente de qualquer observação dessa conclusão. Os retornos de chamada (o observador) fornecidos à função de assinatura não determinam se os recursos estão alocados. É a chamada a subscribesi mesma que potencialmente aloca recursos a montante.
seangwright

Respostas:

981

--- Editar 4 - Recursos adicionais (01/09/2018)

Em um episódio recente de Adventures in Angular, Ben Lesh e Ward Bell discutem as questões sobre como / quando cancelar a inscrição em um componente. A discussão começa por volta das 1:05:30.

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

Em resposta, Ben menciona que há discussões no momento para permitir que os Observables se conectem aos eventos do ciclo de vida do componente Angular e Ward sugere um Observável dos eventos do ciclo de vida que um componente pode assinar como uma maneira de saber quando concluir os Observables mantidos como estado interno do componente.

Dito isto, precisamos principalmente de soluções agora, então aqui estão alguns outros recursos.

  1. Uma recomendação para o takeUntil()padrão do membro da equipe principal do RxJs, Nicholas Jamieson, e uma regra tslint para ajudar a aplicá-lo. https://ncjamieson.com/avoiding-takeuntil-leaks/

  2. Pacote npm leve que expõe um operador Observable que usa uma instância de componente (this ) como parâmetro e cancela automaticamente a inscrição durante ngOnDestroy. https://github.com/NetanelBasal/ngx-take-until-destroy

  3. Outra variação do exposto acima com ergonomia um pouco melhor se você não estiver executando o AOT (mas todos nós deveríamos estar executando o AOT agora). https://github.com/smnbbrv/ngx-rx-collector

  4. Diretiva personalizada *ngSubscribe que funciona como canal assíncrono, mas cria uma exibição incorporada no seu modelo para que você possa consultar o valor 'desembrulhado' em todo o modelo. https://netbasal.com/diy-subscription-handling-directive-in-angular-c8f6e762697f

Menciono em um comentário no blog de Nicholas que o uso excessivo de takeUntil()pode ser um sinal de que seu componente está tentando fazer muito e que a separação dos componentes existentes em componentes de recursos e de apresentação deve ser considerada. Você pode então| async observar o componente Observável do componente InputApresentacional, o que significa que nenhuma assinatura é necessária em nenhum lugar. Leia mais sobre essa abordagem aqui

--- Editar 3 - A solução 'oficial' (09/04/2017)

Conversei com Ward Bell sobre esta questão na NGConf (eu até lhe mostrei a resposta que ele dizia estar correta), mas ele me disse que a equipe de documentos da Angular tinha uma solução para essa pergunta que não é publicada (embora eles estejam trabalhando para aprová-la) ) Ele também me disse que eu poderia atualizar minha resposta do SO com a próxima recomendação oficial.

A solução que todos devemos usar daqui para frente é adicionar um private ngUnsubscribe = new Subject();campo a todos os componentes que possuem .subscribe()chamadas para Observables dentro do código de classe.

Então chamamos this.ngUnsubscribe.next(); this.ngUnsubscribe.complete();nossos ngOnDestroy()métodos.

O segredo (como já observado pelo @metamaker ) é ligar takeUntil(this.ngUnsubscribe)antes de cada uma das nossas .subscribe()chamadas, o que garantirá que todas as assinaturas serão limpas quando o componente for destruído.

Exemplo:

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: É importante adicionar otakeUntil operador como o último para evitar vazamentos com observáveis ​​intermediários na cadeia do operador.

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

Fonte 5

O tutorial Angular, o capítulo Roteamento, agora declara o seguinte: "O roteador gerencia os observáveis ​​que fornece e localiza as assinaturas. As assinaturas são limpas quando o componente é destruído, protegendo contra vazamentos de memória, para que não seja necessário cancelar a assinatura de a rota params Observable. " - Mark Rajcok

Aqui está uma discussão sobre as questões do Github para os documentos angulares sobre roteadores observáveis, onde Ward Bell menciona que há esclarecimentos para tudo isso.

--- Editar 1

Fonte 4

Neste vídeo do NgEurope, Rob Wormald também diz que não é necessário cancelar a assinatura do Router Observables. Ele também menciona o httpserviço e ActivatedRoute.params, neste vídeo, de novembro de 2016 .

--- Resposta original

TLDR:

Para esta pergunta, existem (2) tipos de Observables- valor finito e valor infinito .

http Observablesproduza valores finitos (1) e algo como um DOM event listener Observablesproduz valores infinitos .

Se você chamar manualmente subscribe(sem usar tubulação assíncrona), em seguida, unsubscribea partir infinito Observables .

Não se preocupe com os finitos , RxJscuidará deles.

Fonte 1

Encontrei uma resposta de Rob Wormald em Gitter da Angular aqui .

Ele afirma (eu reorganizei para maior clareza e ênfase é minha)

se for uma sequência de valor único (como uma solicitação http), a limpeza manual é desnecessária (supondo que você assine o controlador manualmente)

eu deveria dizer "se é uma sequência que termina " (das quais seqüências de valor único, a la http, são uma)

se for uma sequência infinita , cancele a inscrição, o que o canal assíncrono faz por você

Ele também menciona neste vídeo do YouTube sobre Observables que they clean up after themselves... no contexto dos Observables que complete(como Promises, que sempre são concluídas porque sempre produzem 1 valor e terminam - nunca nos preocupamos em cancelar a inscrição da Promises para garantir que eles limpem o xhrevento ouvintes, certo?).

Fonte 2

Também no guia Rangle para Angular 2, ele lê

Na maioria dos casos, não precisaremos chamar explicitamente o método de cancelamento de inscrição, a menos que desejemos cancelar mais cedo ou nosso Observable tenha uma vida útil mais longa do que nossa assinatura. O comportamento padrão dos operadores Observáveis ​​é descartar a assinatura assim que as mensagens .complete () ou .error () forem publicadas. Lembre-se de que o RxJS foi projetado para ser usado da maneira "atire e esqueça" na maioria das vezes.

Quando a frase se our Observable has a longer lifespan than our subscriptionaplica?

Aplica-se quando uma assinatura é criada dentro de um componente que é destruído antes (ou não 'muito' antes) da Observableconclusão.

Eu li isso como significando que se subscrevermos uma httpsolicitação ou um observável que emita 10 valores e nosso componente for destruído antes que a httpsolicitação retorne ou que os 10 valores tenham sido emitidos, ainda estamos ok!

Quando a solicitação retornar ou o décimo valor finalmente for emitido, a Observableconclusão será concluída e todos os recursos serão limpos.

Fonte 3

Se olharmos para este exemplo a partir do mesmo guia Rangle, podemos ver que o Subscriptionto route.paramsrequer um unsubscribe()porque não sabemos quando eles paramsirão parar de mudar (emitindo novos valores).

O componente pode ser destruído navegando para longe; nesse caso, os parâmetros de rota provavelmente ainda estarão mudando (eles podem tecnicamente mudar até o aplicativo terminar) e os recursos alocados na assinatura ainda serão alocados porque não houve a completion.

seangwright
fonte
16
A chamada complete()por si só não parece limpar as assinaturas. No entanto, chamando next()e depois o complete()faz, acredito que takeUntil()só para quando um valor é produzido, não quando a sequência termina.
Firefly
3
@seangwright Um teste rápido com um membro do tipo Subjectdentro de um componente e alternando-o ngIfpara acionar ngOnInite ngOnDestroymostrar que o sujeito e suas assinaturas nunca serão concluídas ou descartadas ( finallyvinculado um operador à assinatura). Devo chamar Subject.complete()em ngOnDestroy, por isso, as inscrições podem limpar depois de si.
Lars
4
Seu --- Edit 3 é muito perspicaz, obrigado! Só tenho uma pergunta de acompanhamento: se usar a takeUnitlabordagem, nunca precisaremos cancelar manualmente a inscrição de quaisquer observáveis? É esse o caso? Além disso, por que precisamos ligar next()para ngOnDestroy, por que não apenas ligar complete()?
uglycode
7
@seangwright Isso é decepcionante; o clichê adicional é irritante.
Spongessuck
4
Editar 3 discutido em contexto de eventos em medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87
HankCa
90

Você não precisa ter várias assinaturas e cancelar a assinatura manualmente. Use a combinação Subject e takeUntil para lidar com assinaturas como um chefe:

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

A abordagem alternativa , proposta por @acumartini nos comentários , usa takeWhile em vez de takeUntil . Você pode preferir, mas lembre-se de que dessa maneira sua execução Observable não será cancelada no ngDestroy do seu componente (por exemplo, quando você faz cálculos demorados ou espera os dados do servidor). O método, baseado em takeUntil , não tem essa desvantagem e leva ao cancelamento imediato da solicitação. Agradecemos a @AlexChe pela explicação detalhada nos comentários .

Então aqui está o 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
fonte
2
Se ele apenas usar um bool para manter o estado, como fazer "takeUntil" funcionar como esperado?
Val
6
Eu acho que há uma diferença significativa entre usar takeUntile takeWhile. O primeiro cancela a assinatura da fonte observável imediatamente quando acionado, enquanto o último cancela a assinatura apenas assim que o próximo valor é produzido pela fonte observável. Se produzir um valor pela fonte observável é uma operação que consome recursos, a escolha entre as duas pode ir além da preferência de estilo. Veja o coragem
Alex Che
2
@AlexChe obrigado por fornecer coragem interessante! Este é um ponto muito válido para uso geral de takeUntilvs takeWhile, no entanto, não para o nosso caso específico. Quando precisamos cancelar a inscrição de ouvintes na destruição de componentes , estamos apenas checando o valor booleano como () => aliveem takeWhile, para que qualquer operação que consome tempo / memória não seja usada e a diferença seja basicamente sobre estilo (ofc, nesse caso específico).
Metamaker #
2
@metamaker Diga, em nosso componente, assinamos um Observable, que extrai internamente algumas moedas criptográficas e dispara um nextevento para cada moeda extraída, e a mineração de uma moeda leva um dia. Com takeUntilo cancelamento da assinatura, a mineração de origem será Observableimediatamente ngOnDestroychamada uma vez durante a destruição de nossos componentes. Assim, a Observablefunção de mineração pode cancelar sua operação imediatamente durante esse processo.
Alex Che
2
OTOH, se usarmos takeWhile, no ngOnDestoryapenas definimos a variável booleana. Mas a Observablefunção de mineração ainda pode funcionar por até um dia e, somente então, durante a nextchamada, ela perceberá que não há assinaturas ativas e precisará cancelar.
Alex Che
76

A classe Subscription possui um recurso interessante:

Representa um recurso descartável, como a execução de um Observável. Uma Assinatura possui um método importante, cancelar assinatura, que não exige argumentos e apenas descarta o recurso mantido pela assinatura.
Além disso, as assinaturas podem ser agrupadas por meio do método add (), que anexará uma assinatura filha à assinatura atual. Quando uma assinatura é cancelada, todos os seus filhos (e netos) também serão cancelados.

Você pode criar um objeto de Inscrição agregado que agrupe todas as suas assinaturas. Você faz isso criando uma assinatura vazia e adicionando assinaturas a ela usando seu add()método. Quando seu componente é destruído, você só precisa cancelar a assinatura 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
fonte
1
Eu estou usando essa abordagem. Querendo saber se isso é melhor do que usar a abordagem com takeUntil (), como na resposta aceita .. desvantagens?
Manuel Di Iorio
1
Não tenho inconvenientes. Eu não acho que isso seja melhor, apenas diferente.
Steven Liekens
3
Consulte medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 para obter mais discussões sobre a takeUntilabordagem oficial versus essa abordagem de coleta de assinaturas e chamadas unsubscribe. (Esta abordagem parece muito mais limpo para mim.)
Josh Kelley
4
Um pequeno benefício desta resposta: você não precisa verificar se this.subscriptionsé nulo
user2023861
2
Apenas evite o encadeamento de métodos add, como sub = subsciption.add(..).add(..)porque, em muitos casos, ele produz resultados inesperados github.com/ReactiveX/rxjs/issues/2769#issuecomment-345636477
Evgeniy Generalov
32

Algumas das práticas recomendadas para cancelar a assinatura de observáveis ​​dentro dos componentes Angular:

Uma citação de Routing & Navigation

Ao assinar um observável em um componente, você quase sempre decide cancelar a assinatura quando o componente é destruído.

Existem alguns observáveis ​​excepcionais onde isso não é necessário. Os observáveis ​​ActivatedRoute estão entre as exceções.

O ActivatedRoute e seus observáveis ​​são isolados do próprio roteador. O roteador destrói um componente roteado quando não é mais necessário e o ActivatedRoute injetado morre com ele.

Fique à vontade para cancelar a inscrição de qualquer maneira. É inofensivo e nunca é uma prática ruim.

E ao responder aos seguintes links:

Reuni algumas das práticas recomendadas para cancelar a assinatura de observáveis ​​dentro dos componentes Angular para compartilhar com você:

  • httpO cancelamento da inscrição observável é condicional e devemos considerar os efeitos do 'retorno de chamada de assinatura' sendo executado depois que o componente é destruído caso a caso. Sabemos que o angular cancela e limpa o httppróprio observável (1) , (2) . Embora isso seja verdade da perspectiva dos recursos, ele conta apenas metade da história. Digamos que estamos falando sobre chamar diretamente httpde dentro de um componente e ohttp resposta levou mais tempo do que o necessário para que o usuário fechasse o componente. osubscribe() manipulador ainda será chamado, mesmo que o componente seja fechado e destruído. Isso pode ter efeitos colaterais indesejados e, nos piores cenários, deixar o estado do aplicativo quebrado. Também pode causar exceções se o código no retorno de chamada tentar chamar algo que acabou de ser descartado. No entanto, ao mesmo tempo, ocasionalmente, eles são desejados. Por exemplo, digamos que você esteja criando um cliente de e-mail e acionando um som quando o e-mail terminar de enviar.)
  • Não há necessidade de cancelar a assinatura de observáveis ​​que são concluídos ou com erro. No entanto, não há mal em fazê-lo (7) .
  • Usar AsyncPipe o máximo possível, pois ele cancela automaticamente a assinatura do observado na destruição de componentes.
  • Cancele a inscrição nos ActivatedRouteobserváveis, como route.paramsse eles estivessem inscritos em um componente aninhado (Adicionado dentro da tpl com o seletor de componente) ou componente dinâmico, pois eles podem ser inscritos muitas vezes, desde que o componente pai / host exista. Não é necessário cancelar a assinatura deles em outros cenários, como mencionado na citação acima, deRouting & Navigation documentos.
  • Cancele a inscrição em observáveis ​​globais compartilhados entre componentes expostos por meio de um serviço Angular, por exemplo, pois eles podem ser registrados várias vezes, desde que o componente seja inicializado.
  • Não é necessário cancelar a inscrição de observáveis ​​internos de um serviço com escopo de aplicativo, pois esse serviço nunca é destruído, a menos que todo o aplicativo seja destruído, não há motivo real para cancelar a inscrição e não há chance de vazamento de memória. (6) .

    Nota: Em relação aos serviços com escopo definido, ou seja, provedores de componentes, eles são destruídos quando o componente é destruído. Nesse caso, se subscrevermos qualquer elemento observável dentro deste provedor, considere cancelar a inscrição usando o OnDestroygancho do ciclo de vida que será chamado quando o serviço for destruído, de acordo com os documentos.
  • Use uma técnica abstrata para evitar qualquer confusão de código resultante de cancelamentos de inscrição. Você pode gerenciar suas assinaturas com takeUntil (3) ou usar este npm pacote mencionado em (4) A maneira mais fácil de cancelar a assinatura de Observables in Angular .
  • Sempre desinscreva-se de FormGroupobserváveis ​​como form.valueChangeseform.statusChanges
  • Sempre cancele a assinatura de observáveis ​​de Renderer2serviço comorenderer2.listen
  • Cancele a assinatura de todos os outros itens observáveis ​​como uma etapa de proteção contra vazamento de memória até que o Angular Docs nos diga explicitamente quais itens desnecessários não precisam ser registrados (verifique o problema: (5) Documentação para cancelamento da inscrição no RxJS (aberto) ).
  • Bônus: sempre use as formas angulares para vincular eventos, como HostListenero angular se preocupa em remover os ouvintes de eventos, se necessário, e evita qualquer possível vazamento de memória devido a ligações de eventos.

Uma boa dica final : se você não sabe se um observável está sendo cancelado / concluído automaticamente ou não, adicione um completeretorno de chamada subscribe(...)e verifique se ele é chamado quando o componente é destruído.

Mouneer
fonte
A resposta para o número 6 não está certa. Os serviços são destruídos e ngOnDestroysão chamados quando o serviço é fornecido em um nível diferente do nível raiz, por exemplo, fornecido explicitamente em um componente que é removido posteriormente. Nestes casos, você deve cancelar a inscrição dos serviços observáveis internas
Drenai
@ Drenai, obrigado pelo seu comentário e educadamente não concordo. Se um componente for destruído, o componente, o serviço e o observável serão GCed e a anulação da assinatura será inútil nesse caso, a menos que você mantenha uma referência para o observável em qualquer lugar longe do componente (o que não é lógico para vazar os estados do componente globalmente) apesar escopo do serviço para o componente)
Mouneer
Se o serviço que está sendo destruído tiver uma assinatura de um observável pertencente a outro serviço mais alto na hierarquia de DI, o GC não ocorrerá. Evite esse cenário, cancelando a inscrição ngOnDestroy, que é sempre chamada quando os serviços são destruídos github.com/angular/angular/commit/…
Drenai
1
@ Drenai, verifique a resposta atualizada.
Mouneer
3
@ Tim Primeiro de tudo, Feel free to unsubscribe anyway. It is harmless and never a bad practice.e em relação à sua pergunta, depende. Se o componente filho for iniciado várias vezes (por exemplo, adicionado dentro ngIfou sendo carregado dinamicamente), você deverá cancelar a inscrição para evitar adicionar várias assinaturas ao mesmo observador. Caso contrário, não há necessidade. Mas eu prefiro cancelar a inscrição dentro do componente filho, pois isso o torna mais reutilizável e isolado de como ele poderia ser usado.
Mouneer
18

Depende. Se ligando someObservable.subscribe(), você começa a reter algum recurso que deve ser liberado manualmente quando o ciclo de vida do seu componente terminar, então você deve ligar theSubscription.unsubscribe()para evitar vazamento de memória.

Vamos dar uma olhada em seus exemplos:

getHero()retorna o resultado de http.get(). Se você olhar para o código-fonte angular 2 , http.get()cria dois ouvintes de evento:

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

e ligando unsubscribe(), você pode cancelar a solicitação e os ouvintes:

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

Observe que _xhré específico da plataforma, mas acho que é seguro assumir que é um XMLHttpRequest()no seu caso.

Normalmente, isso é evidência suficiente para justificar uma unsubscribe()chamada manual . Porém, de acordo com esta especificação do WHATWG , o itemXMLHttpRequest() está sujeito à coleta de lixo assim que é "concluída", mesmo se houver ouvintes de eventos anexados a ele. Acho que é por isso que o guia oficial angular 2 omite unsubscribe()e permite que o GC limpe os ouvintes.

Quanto ao seu segundo exemplo, isso depende da implementação de params. A partir de hoje, o guia oficial angular não mostra mais o cancelamento da inscrição params. Eu olhei para o src novamente e descobri que paramsé apenas um BehaviorSubject . Como nenhum ouvinte ou timer de evento foi usado e nenhuma variável global foi criada, deve ser seguro omitir unsubscribe().

O ponto principal da sua pergunta é que sempre chame unsubscribe()como proteção contra vazamento de memória, a menos que você tenha certeza de que a execução do observável não cria variáveis ​​globais, adiciona ouvintes de eventos, configura temporizadores ou faz qualquer outra coisa que resulte em vazamentos de memória .

Em caso de dúvida, examine a implementação desse observável. Se o observável tiver escrito alguma lógica de limpeza na sua unsubscribe(), que geralmente é a função retornada pelo construtor, você tem bons motivos para considerar seriamente a chamada unsubscribe().

Chuanqi Sun
fonte
6

A documentação oficial do Angular 2 fornece uma explicação para quando cancelar a inscrição e quando pode ser ignorada com segurança. Dê uma olhada neste link:

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

Procure o parágrafo com o título Pais e filhos se comunicam por meio de um serviço e, em seguida, a caixa azul:

Observe que capturamos a assinatura e cancelamos a assinatura quando o AstronautComponent é destruído. Esta é uma etapa de proteção contra vazamento de memória. Não há risco real neste aplicativo, porque a vida útil de um AstronautComponent é a mesma que a vida útil do aplicativo. Isso nem sempre seria verdade em uma aplicação mais complexa.

Não adicionamos essa proteção ao MissionControlComponent porque, como pai, ele controla a vida útil do MissionService.

Espero que isso ajude você.

Cerny
fonte
3
como um componente, você nunca sabe se é criança ou não. portanto, você deve sempre cancelar a inscrição de assinaturas como prática recomendada.
SeriousM
1
O ponto sobre MissionControlComponent não é realmente sobre se é um pai ou não, é que o próprio componente fornece o serviço. Quando o MissionControl é destruído, o serviço e as referências à instância do serviço também são destruídos, portanto não há possibilidade de vazamento.
ender
6

Baseado em: Usando a herança de classe para ligar ao ciclo de vida do componente Angular 2

Outra abordagem genérica:

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
  }

}

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

JoG
fonte
1
Isso NÃO funciona corretamente. Tenha cuidado ao usar esta solução. Está faltando uma this.componentDestroyed$.next()chamada como a solução aceita por sean acima ...
philn
4

A resposta oficial do Edit # 3 (e variações) funciona bem, mas o que me impressiona é a "confusão" da lógica de negócios em torno da assinatura observável.

Aqui está outra abordagem usando wrappers.

Warining: código experimental

O arquivo subscribeAndGuard.ts é usado para criar uma nova extensão Observable para quebrar .subscribe()e dentro dela para quebrar ngOnDestroy().
O uso é o mesmo que .subscribe(), exceto por um primeiro parâmetro adicional que faz referência ao 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;
  }
}

Aqui está um componente com duas assinaturas, uma com o wrapper e outra sem. A única ressalva é que ele deve implementar o OnDestroy (com corpo vazio, se desejado), caso contrário, o Angular não sabe chamar a versão agrupada .

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

Um demo plunker está aqui

Uma observação adicional: Re Edit 3 - A solução 'Oficial', isso pode ser simplificado usando takeWhile () em vez de takeUntil () antes das assinaturas, e um booleano simples em vez de outro Observable no 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
fonte
3

Como a solução de seangwright (Edição 3) parece ser muito útil, também achei difícil incluir esse recurso no componente base e sugerir que outros colegas de equipe do projeto se lembrem de chamar super () no ngOnDestroy para ativar esse recurso.

Esta resposta fornece uma maneira de liberar-se da chamada super e tornar "componentDestroyed $" um núcleo do 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() {}
}

E então você pode usar esse recurso livremente, por exemplo:

@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
fonte
3

Seguindo a resposta de @seangwright , escrevi uma classe abstrata que lida com assinaturas "infinitas" de observáveis ​​em 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 usá-lo, apenas estenda-o em seu componente angular e chame o subscribe()método da seguinte maneira:

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

Ele também aceita o erro e realiza retornos de chamada como de costume, um objeto observador ou nenhum retorno de chamada. Lembre- super.ngOnDestroy()se de ligar se você também estiver implementando esse método no componente filho.

Encontre aqui uma referência adicional de Ben Lesh: RxJS: Não cancele a inscrição .

Mau Muñoz
fonte
2

Eu tentei a solução de seangwright (Editar 3)

Isso não está funcionando para o Observable criado por timer ou intervalo.

No entanto, consegui trabalhar usando outra abordagem:

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
fonte
2

Gosto das duas últimas respostas, mas tive um problema se a subclasse mencionada "this"em ngOnDestroy.

Eu o modifiquei para ser isso e parece que resolveu esse 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
fonte
você precisa usar a função de seta para ligar o 'this':this.ngOnDestroy = () => { f.bind(this)(); this.componentDestroyed$.complete(); };
Damsorian
2

Caso seja necessário cancelar a assinatura, o operador a seguir para o método de tubo observável pode ser usado

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

pode ser usado assim:

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

O operador envolve o método ngOnDestroy do componente.

Importante: o operador deve ser o último em tubo observável.

Oleg Polezky
fonte
Isso funcionou muito bem, no entanto, a atualização para o angular 9 parece matá-lo. Alguém sabe o porquê?
ymerej 28/02
1

Você geralmente precisa cancelar a assinatura quando os componentes forem destruídos, mas o Angular vai lidar com isso cada vez mais, como, por exemplo, na nova versão secundária do Angular4, eles têm esta seção para rotear o cancelamento da inscrição:

Você precisa cancelar a inscrição?

Conforme descrito na seção ActivatedRoute: a seção completa de informações sobre rotas da página Roteamento e navegação, o roteador gerencia os observáveis ​​que ele fornece e localiza as assinaturas. As assinaturas são limpas quando o componente é destruído, protegendo contra vazamentos de memória, para que você não precise cancelar a assinatura da rota paramMap Observable.

Além disso, o exemplo abaixo é um bom exemplo da Angular para criar um componente e destruí-lo depois. Veja como o componente implementa o OnDestroy. Se você precisar do OnInit, também pode implementá-lo no seu 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
fonte
3
Confuso. O que você está dizendo aqui? Você (documentos / notas recentes do Angular) parece dizer que o Angular cuida disso e, posteriormente, para confirmar que o cancelamento da inscrição é um bom padrão. Obrigado.
Jamie
1

Outra pequena adição às situações acima mencionadas é:

  • Sempre cancele a inscrição, quando novos valores no fluxo inscrito não forem mais necessários ou não importarem, resultará em um número menor de gatilhos e aumentará o desempenho em alguns casos. Casos como componentes em que os dados / eventos inscritos não existem mais ou uma nova assinatura para um fluxo totalmente novo (atualização etc.) é um bom exemplo de cancelamento de inscrição.
Krishna Ganeriwal
fonte
0

no aplicativo SPA na função ngOnDestroy (angular lifeCycle) Para cada assinatura, é necessário cancelar a assinatura . vantagem => para impedir que o estado fique muito pesado.

por exemplo: no componente1:

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

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

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

em serviço:

import {BehaviorSubject} from 'rxjs/BehaviorSubject';

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

no 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
fonte
0

Para lidar com a assinatura, uso uma classe "Unsubscriber".

Aqui está a 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();
  }
}

E você pode usar esta classe em qualquer componente / Serviço / Efeito etc.

Exemplo:

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

    this.addSubscription(subscription);
}
Pratiyush
fonte
0

Você pode usar a Subscriptionclasse mais recente para cancelar a inscrição no Observable com código não tão confuso.

Podemos fazer isso com, normal variablemas seráoverride the last subscription em cada nova assinatura. Portanto, evite isso, e essa abordagem é muito útil quando você está lidando com um número maior de Obseravables e tipos de Obeservables como BehavoiurSubjecteSubject

Inscrição

Representa um recurso descartável, como a execução de um Observável. Uma Assinatura possui um método importante, cancelar assinatura, que não exige argumentos e apenas descarta o recurso mantido pela assinatura.

você pode usar isso de duas maneiras,

  • você pode enviar diretamente a assinatura para o Subscription Array

     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());
      }
    
  • usando add()deSubscription

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

UMA Subscription pode manter assinaturas filhas e cancelar todas elas com segurança. Este método lida com possíveis erros (por exemplo, se alguma assinatura filha for nula).

Espero que isto ajude.. :)

ganesh045
fonte
0

O pacote SubSink, uma solução fácil e consistente para cancelar a inscrição

Como ninguém mais o mencionou, quero recomendar o pacote Subsink criado por Ward Bell: https://github.com/wardbell/subsink#readme .

Eu tenho usado em um projeto onde somos vários desenvolvedores. Ajuda muito ter uma maneira consistente que funcione em todas as situações.

SnorreDan
fonte
0

Para observáveis ​​que são concluídos diretamente após a emissão do resultado, como, AsyncSubjectpor exemplo, observáveis ​​de solicitações de HTTP, você não precisa cancelar a inscrição. Não custa chamar unsubscribe(), mas se o observável é closedo método de cancelamento da assinatura simplesmente não fará nada :

if (this.closed) {
  return;
}

Quando você tem observáveis ​​de longa duração que emitem vários valores ao longo do tempo (como por exemplo, a BehaviorSubjectou a ReplaySubject), é necessário cancelar a assinatura para evitar vazamentos de memória.

Você pode criar facilmente um observável que é concluído diretamente após emitir um resultado de observáveis ​​de longa duração usando um operador de tubo. Em algumas respostas aqui o take(1)tubo é mencionado. Mas eu prefiro o first()cachimbo . A diferença take(1)é que ele irá:

entregue um EmptyErrorretorno de chamada de erro do Observador se o Observável for concluído antes que qualquer próxima notificação seja enviada.

Outra vantagem do primeiro canal é que você pode passar um predicado que o ajudará a retornar o primeiro valor que atenda a certos critérios:

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

O First será concluído diretamente após a emissão do primeiro valor (ou ao passar um argumento de função, o primeiro valor que satisfaça seu predicado), para que não seja necessário cancelar a inscrição.

Às vezes, você não tem certeza se tem uma vida longa observável ou não. Não estou dizendo que é uma boa prática, mas você pode sempre adicionar o firstpipe apenas para garantir que não precisará cancelar manualmente a inscrição. Adicionar um firsttubo adicional a um observável que emitirá apenas um valor não prejudica.

Durante o desenvolvimento, você pode usar o singlecanal que falhará se a origem observável emitir vários eventos. Isso pode ajudá-lo a explorar o tipo de observável e se é necessário cancelar a inscrição ou não.

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

Os dois canais firste singleparecem muito semelhantes, podem ter um predicado opcional, mas as diferenças são importantes e bem resumidas nesta resposta do stackoverflow aqui :

Primeiro

Será emitido assim que o primeiro item aparecer. Será concluído logo depois disso.

solteiro

Falhará se a fonte observável emitir vários eventos.


Observe que tentei ser o mais preciso e completo possível na minha resposta com referências à documentação oficial, mas comente se algo importante estiver faltando ...

Wilt
fonte
-1

--- Atualize a solução Angular 9 e Rxjs 6

  1. Usando unsubscribeno ngDestroyciclo de vida do 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. Usando takeUntilem 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 alguma ação que você chama, ngOnInitisso acontece apenas uma vez quando o componente init.
class SampleComponent implements OnInit {

  private sampleObservable$: Observable<any>;

  constructor () {}

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

Nós também temos asynccachimbo. Mas, este uso no modelo (não no componente Angular).

Hoang Tran Son
fonte
Seu primeiro exemplo está incompleto.
Paul-Sebastian Manole