6 maneiras de cancelar a inscrição de Observables em Angular


O verso de uma assinatura Observable


Observables possui um método de assinatura chamado com uma função de retorno de chamada para obter os valores enviados ( emitidos ) para o Observable. No Angular, é usado em componentes / diretivas e, principalmente, no módulo roteador, NgRx e HTTP.


Se assinarmos um fluxo, ele permanecerá aberto e será chamado sempre que valores de qualquer parte do aplicativo forem transferidos para ele, até que seja fechado chamando cancelar assinatura .


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

Nesta implementação, usamos um intervalo para enviar valores a cada segundo. Nós o subscrevemos para obter o valor enviado e nossa função de retorno de chamada grava o resultado no console do navegador.


Agora, se o AppComponent for destruído, por exemplo, depois de sair do componente ou usar o método destroy () , ainda veremos o log do console no navegador. Isso ocorre porque, embora o AppComponent tenha sido destruído, a assinatura não foi cancelada.


Se a assinatura não for fechada, a função de retorno de chamada será continuamente chamada, o que levará a um sério vazamento de memória e problemas de desempenho. Para evitar vazamentos, é necessário "cancelar a inscrição" do Observável a cada vez.


1. Usando o método de cancelamento de inscrição


Qualquer Assinatura possui uma função unsubscribe () para liberar recursos e cancelar a execução Observável. Para evitar um vazamento de memória, você deve cancelar a inscrição usando o método de cancelamento de inscrição em Observável.


No Angular, é necessário cancelar a assinatura do Observable quando um componente é destruído. Felizmente, o Angular possui um gancho ngOnDestroy chamado antes da destruição do componente, o que permite que os desenvolvedores garantam que a memória seja limpa, para evitar o congelamento de assinaturas, portas abertas e outros "disparos no pé".


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

Adicionamos ngOnDestroy ao nosso AppComponent e chamamos o método de cancelamento de assinatura em Observable this.subscription . Quando o AppComponent é destruído (clicando no link, no método destroy () etc.), a assinatura não congela, o intervalo será interrompido e não haverá mais logs do console no navegador.


Mas e se tivermos várias assinaturas?


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription1$: Subscription; subscription2$: Subscription; ngOnInit () { const observable1$ = Rx.Observable.interval(1000); const observable2$ = Rx.Observable.interval(400); this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x)); this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x)); } ngOnDestroy() { this.subscription1$.unsubscribe(); this.subscription2$.unsubscribe(); } } 

Há duas assinaturas no AppComponent e ambas não assinadas no gancho ngOnDestroy , impedindo um vazamento de memória.


Você também pode coletar todas as assinaturas em uma matriz e cancelar a assinatura delas em um loop:


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription1$: Subscription; subscription2$: Subscription; subscriptions: Subscription[] = []; ngOnInit () { const observable1$ = Rx.Observable.interval(1000); const observable2$ = Rx.Observable.interval(400); this.subscription1$ = observable.subscribe(x => console.log("From interval 1000" x)); this.subscription2$ = observable.subscribe(x => console.log("From interval 400" x)); this.subscriptions.push(this.subscription1$); this.subscriptions.push(this.subscription2$); } ngOnDestroy() { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } } 

O método de inscrição retorna um objeto RxJS do tipo Subscription. É um recurso único. As assinaturas podem ser agrupadas usando o método add , que anexará a assinatura filha à atual. Quando uma assinatura é cancelada, todos os seus filhos também são cancelados. Vamos tentar reescrever nosso AppComponent:


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription: Subscription; ngOnInit () { const observable1$ = Rx.Observable.interval(1000); const observable2$ = Rx.Observable.interval(400); const subscription1$ = observable.subscribe(x => console.log("From interval 1000" x)); const subscription2$ = observable.subscribe(x => console.log("From interval 400" x)); this.subscription.add(subscription1$); this.subscription.add(subscription2$); } ngOnDestroy() { this.subscription.unsubscribe() } } 

Então, escreveremos this.subscripton1 $ e this.subscripton2 $ no momento da destruição do componente.


2. Usando o Async | Pipe


O pipe async assina Observable ou Promise e retorna o último valor passado. Quando um novo valor é enviado, o pipe async verifica o componente quanto ao rastreamento de alterações. Se o componente for destruído, a sincronização de canal será automaticamente cancelada.


 @Component({ ..., template: ` <div> Interval: {{observable$ | async}} </div> ` }) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); } } 

Após a inicialização, o AppComponent criará um Observable a partir do método interval. No padrão $ observável , a async é passada. Ele assina $ observável e exibe seu valor no DOM. Ele também cancelará a inscrição quando o AppComponent for destruído. A assíncrona de pipe em sua classe contém o gancho ngOnDestroy , portanto será chamado quando sua exibição for destruída.
O pipe assíncrono é muito conveniente de usar, porque ele próprio assinará o cancelamento da assinatura e cancelará a assinatura deles. E agora não podemos nos preocupar se esquecermos de cancelar a inscrição no ngOnDestroy .


3. Usando as instruções take * do RxJS


O RxJS contém operadores úteis que você pode usar de forma declarativa para cancelar a inscrição em nosso projeto Angular. Um deles é o operador da família * take **:


  • tomar (n)
  • takeUntil (notificador)
  • takeWhile (predicado)

tomar (n)
Este operador emite o número especificado de vezes e termina da assinatura original. Na maioria das vezes, um (1) é passado para levar para assinatura e saída.


Esse operador é útil se quisermos que o Observable passe um valor uma vez e, em seguida, desinscreva-se do fluxo:


 @Component({...}) export class AppComponent implements OnInit { subscription$; ngOnInit () { const observable$ = Rx.Observable.interval(1000); this.subscription$ = observable$.pipe(take(1)). subscribe(x => console.log(x)); } } 

A assinatura $ será cancelada quando o intervalo ultrapassar o primeiro valor.


Observação : mesmo que o AppComponent seja destruído, a assinatura $ não cancelará a assinatura até que o intervalo passe o valor. Portanto, é melhor garantir que tudo esteja cancelado no gancho ngOnDestroy :


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

takeUntil (notificador)
Esta declaração emite os valores do Observável original até o notificador enviar uma mensagem de conclusão.


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { notifier = new Subject(); ngOnInit () { const observable$ = Rx.Observable.interval(1000); observable$.pipe(takeUntil(this.notifier)).subscribe(x => console.log(x)); } ngOnDestroy() { this.notifier.next(); this.notifier.complete(); } } 

Temos um assunto adicional para notificações, que enviará um comando para cancelar a inscrição this.subscription . Somos observáveis ​​em takeUntil até que sejam assinados. O TakeUntil emitirá mensagens de intervalo até que o notificador cancele a assinatura de $ observável . É melhor colocar o notificador no gancho ngOnDestroy .


takeWhile (predicado)
Essa instrução emitirá valores observáveis ​​desde que correspondam à condição de predicado.


 @Component({...}) export class AppComponent implements OnInit { ngOnInit () { const observable$ = Rx.Observable.interval(1000); observable$.pipe(takeWhile(value => value < 10)).subscribe(x => console.log(x)); } } 

Nós canalizamos $ observáveis com o operador takeWhile, que enviará valores desde que sejam menores que 10. Se um valor maior ou igual a 10 chegar, o operador cancelará a inscrição.
É importante entender que a assinatura $ observável ficará aberta até o intervalo dar 10. Portanto, por segurança, adicionamos o gancho ngOnDestroy para cancelar a assinatura de $ observável quando o componente é destruído.


 @Component({…}) export class AppComponent implements OnInit, OnDestroy { subscription$; ngOnInit () { var observable$ = Rx.Observable.interval(1000); this.subscription$ = observable$.pipe(takeWhile(value => value < 10)).subscribe(x => console.log(x)); } ngOnDestroy() { this.subscription$.unsubscribe(); } } 

4. Usando o primeiro operador RxJS


Essa declaração é semelhante à combinação de take (1) e takeWhile.


Se for chamado sem um parâmetro, o primeiro valor emitido é Observable e termina. Se for chamado com uma função de predicado, então emitir - é o primeiro valor do Observável original, que corresponde à condição da função de predicado, e termina.


 @Component({...}) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable = Rx.Observable.interval(1000); this.observable$.pipe(first()).subscribe(x => console.log(x)); } } 

$ observável terminará se o intervalo passar seu primeiro valor. Isso significa que no console veremos apenas 1 mensagem de log.


 @Component({...}) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); this.observable$.pipe(first(val => val === 10)).subscribe(x => console.log(x)); } } 

Aqui, primeiro não será emitido valores até que o intervalo passe 10 e, em seguida, conclua $ observável . No console, veremos apenas uma mensagem.


No primeiro exemplo, se o AppComponent for destruído antes que o primeiro obtenha o valor de $ observável , a assinatura ainda estará aberta até que a primeira mensagem seja recebida.
Além disso, no segundo exemplo, se o AppComponent for destruído antes do intervalo retornar um valor correspondente à condição do operador, a assinatura continuará aberta até o intervalo retornar 10. Portanto, para garantir a segurança, devemos cancelar explicitamente as assinaturas em hook ngOnDestroy .


5. Usando o Decorator para automatizar o cancelamento da inscrição


Somos todos humanos, tendemos a esquecer. A maioria dos métodos anteriores se baseia no gancho ngOnDestroy para garantir que a assinatura seja limpa antes de destruir o componente. Mas podemos esquecer de registrá-los no ngOnDestroy - talvez por causa do prazo ou pelo cliente nervoso que sabe onde você mora ...


Nesse caso, podemos usar Decoradores em nossos projetos Angular para cancelar automaticamente a inscrição de todas as assinaturas do componente.


Aqui está um exemplo de uma implementação tão útil:


 function AutoUnsub() { return function(constructor) { const orig = constructor.prototype.ngOnDestroy; constructor.prototype.ngOnDestroy = function() { for(let prop in this) { const property = this[prop]; if(typeof property.subscribe === "function") { property.unsubscribe(); } } orig.apply(); } } } 

Este AutoUnsub é um decorador que pode ser aplicado a classes em nosso projeto Angular. Como você pode ver, ele salva o gancho ngOnDestroy original, depois cria um novo e o conecta à classe à qual é aplicado. Dessa forma, quando a classe é destruída, um novo gancho é chamado. Sua função examina as propriedades da classe e, se encontrar um Observable, cancela sua inscrição. Em seguida, ele chama o gancho ngOnDestroy original da classe, se houver.


 @Component({...}) @AutoUnsub export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); this.observable$.subscribe(x => console.log(x)) } } 

Nós o aplicamos ao nosso AppComponent e não nos preocupamos mais em esquecer de cancelar a assinatura de $ observável no ngOnDestroy - o decorador fará isso por nós.


Mas esse método também tem uma desvantagem - erros ocorrerão se nosso componente tiver um Observable sem uma assinatura.


6. Usando tslint


Às vezes, uma mensagem do tslint pode ser útil para informar ao console que nossos componentes ou diretivas não declararam o método ngOnDestroy . Você pode adicionar uma regra personalizada ao tslint para avisar o console quando o lint ou build estiver em execução, de que nossos componentes não possuem o gancho ngOnDestroy :


 // ngOnDestroyRule.tsimport * as Lint from "tslint" import * as ts from "typescript"; import * as tsutils from "tsutils"; export class Rule extends Lint.Rules.AbstractRule { public static metadata: Lint.IRuleMetadata = { ruleName: "ng-on-destroy", description: "Enforces ngOnDestory hook on component/directive/pipe classes", optionsDescription: "Not configurable.", options: null, type: "style", typescriptOnly: false } public static FAILURE_STRING = "Class name must have the ngOnDestroy hook"; public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { return this.applyWithWalker(new NgOnDestroyWalker(sourceFile, Rule.metadata.ruleName, void this.getOptions())) } } class NgOnDestroyWalker extends Lint.AbstractWalker { visitClassDeclaration(node: ts.ClassDeclaration) { this.validateMethods(node); } validateMethods(node: ts.ClassDeclaration) { const methodNames = node.members.filter(ts.isMethodDeclaration).map(m => m.name!.getText()); const ngOnDestroyArr = methodNames.filter( methodName => methodName === "ngOnDestroy"); if( ngOnDestroyArr.length === 0) this.addFailureAtNode(node.name, Rule.FAILURE_STRING); } } 

Se tivermos esse componente sem o ngOnDestroy :


 @Component({...}) export class AppComponent implements OnInit { observable$; ngOnInit () { this.observable$ = Rx.Observable.interval(1000); this.observable$.subscribe(x => console.log(x)); } } 

O AppComponent, que é um dos fiapos, alertará sobre o gancho ngOnDestroy ausente:


 $ ng lint Error at app.component.ts 12: Class name must have the ngOnDestroy hook 

Conclusão


Uma assinatura pendente ou aberta pode levar a vazamentos de memória, erros, comportamento indesejado ou desempenho ruim do aplicativo. Para evitar isso, analisamos diferentes maneiras de cancelar a inscrição em projetos Observable in Angular. E o que você deve usar em uma situação específica é com você.

Source: https://habr.com/ru/post/pt484762/


All Articles