É necessário cancelar a assinatura de observáveis ​​criados pelos métodos Http?


211

Você precisa cancelar a inscrição nas chamadas http do Angular 2 para evitar vazamento de memória?

 fetchFilm(index) {
        var sub = this._http.get(`http://example.com`)
            .map(result => result.json())
            .map(json => {
                dispatch(this.receiveFilm(json));
            })
            .subscribe(e=>sub.unsubscribe());
            ...


1
Veja stackoverflow.com/questions/34461842/… (e também os comentários)
Eric Martinez

Respostas:


253

Então a resposta é não, você não. Ng2irá limpá-lo em si.

A fonte de serviço Http, da fonte de back-end Http XHR da Angular:

insira a descrição da imagem aqui

Observe como ele é executado complete()depois de obter o resultado. Isso significa que ele realmente cancela a inscrição ao concluir. Então você não precisa fazer isso sozinho.

Aqui está um teste para validar:

  fetchFilms() {
    return (dispatch) => {
        dispatch(this.requestFilms());

        let observer = this._http.get(`${BASE_URL}`)
            .map(result => result.json())
            .map(json => {
                dispatch(this.receiveFilms(json.results));
                dispatch(this.receiveNumberOfFilms(json.count));
                console.log("2 isUnsubscribed",observer.isUnsubscribed);
                window.setTimeout(() => {
                  console.log("3 isUnsubscribed",observer.isUnsubscribed);
                },10);
            })
            .subscribe();
        console.log("1 isUnsubscribed",observer.isUnsubscribed);
    };
}

Como esperado, você pode ver que ele é sempre cancelado automaticamente após obter o resultado e terminar com os operadores observáveis. Isso acontece em um tempo limite (nº 3) para que possamos verificar o status do observável quando tudo estiver pronto e concluído.

E o resultado

insira a descrição da imagem aqui

Portanto, não haveria vazamento como Ng2cancelamento automático de inscrição!

É bom mencionar: isso Observableé classificado como finite, ao contrário do infinite Observableque é um fluxo infinito de dados que pode ser emitido como DOMclick ouvinte por exemplo.

OBRIGADO, @rubyboy pela ajuda.


17
o que acontece nos casos em que o usuário sai da página antes da resposta chegar ou se a resposta nunca vem antes que o usuário saia da visualização? Isso não causaria um vazamento?
Thibs

1
Eu também gostaria de saber o que foi dito acima.
1984

4
@ 1984 A resposta SEMPRE NÃO SE INSCREVE. Esta resposta está totalmente errada exatamente pelo motivo exposto neste comentário. O usuário que está navegando é um link de memória. Além disso, se o código nessa assinatura for executado após o usuário navegar, poderá causar erros / ter efeitos colaterais inesperados. Costumo usar takeWhile (() => this.componentActive) em todos os observáveis ​​e apenas definir this.componentActive = false no ngOnDestroy para limpar todos os observáveis ​​em um componente.
Lenny

4
O exemplo exibido aqui abrange uma API particular angular profunda. com qualquer outra API privada, ele pode ser alterado com uma atualização sem aviso prévio. A regra geral é: se não estiver oficialmente documentado, não faça nenhuma suposição. Por exemplo, o AsynPipe possui uma documentação clara de que automaticamente se inscreve / cancela sua inscrição. Os documentos do HttpClient não mencionam nada sobre isso.
perfil completo de YoussefTaghlabi

1
@YoussefTaghlabi, FYI, a documentação angular oficial ( angular.io/guide/http ) menciona que: "O AsyncPipe assina (e cancela a inscrição) automaticamente para você". Então, é realmente oficialmente documentado.
Hudgi 11/05/19

96

Sobre o que vocês estão falando!!!

OK, então há duas razões para cancelar a inscrição de qualquer observável. Ninguém parece estar falando muito sobre a segunda razão muito importante!

1) Limpe os recursos. Como já foi dito, este é um problema insignificante para observáveis ​​em HTTP. Apenas vai se limpar.

2) Impedir que o subscribemanipulador seja executado.

(Para HTTP, isso na verdade também cancelará a solicitação no navegador - para que não perca tempo lendo a resposta. Mas isso é um aparte do meu ponto principal abaixo.)

A relevância do número 2 dependerá do que o manipulador de assinaturas fizer:

Se a subscribe()função do manipulador tiver algum tipo de efeito colateral indesejável, seja qual for a chamada que for fechada ou descartada, você deverá cancelar a inscrição (ou adicionar lógica condicional) para impedir que seja executada.

Considere alguns casos:

1) Um formulário de login. Você digita o nome de usuário e a senha e clica em 'Login'. E se o servidor estiver lento e você decidir pressionar Escape para fechar a caixa de diálogo? Provavelmente, você presumirá que não estava logado, mas se a solicitação http retornar depois de pressionar escape, você ainda executará a lógica que tiver lá. Isso pode resultar em um redirecionamento para uma página da conta, sendo definido um cookie de login indesejado ou uma variável de token. Provavelmente não é o que seu usuário esperava.

2) Um formulário 'enviar email'.

Se o subscribe manipulador de 'sendEmail' fizer algo como acionar uma animação 'Seu email é enviado', transfira você para uma página diferente ou tente acessar qualquer coisa que tenha sido descartada, você pode receber exceções ou comportamentos indesejados.

Também tome cuidado para não assumir que unsubscribe()significa 'cancelar'. Uma vez que a mensagem HTTP esteja em andamento unsubscribe(), NÃO cancelará a solicitação HTTP se já tiver atingido seu servidor. Ele apenas cancelará a resposta retornando para você. E o email provavelmente será enviado.

Se você criar a assinatura para enviar o email diretamente dentro de um componente da interface do usuário, provavelmente desejará cancelar a inscrição ao descartar, mas se o email estiver sendo enviado por um serviço centralizado que não seja a UI, provavelmente não será necessário.

3) Um componente angular que é destruído / fechado. Quaisquer http observáveis ​​ainda em execução no momento concluirão e executarão sua lógica, a menos que você cancele a inscrição emonDestroy() . Se as consequências são triviais ou não, dependerá do que você faz no manipulador de assinaturas. Se você tentar atualizar algo que não existe mais, poderá receber um erro.

Às vezes, você pode ter algumas ações que deseja se o componente for descartado e outras não. Por exemplo, talvez você tenha um som de "swoosh" para um email enviado. Você provavelmente deseja que isso seja reproduzido, mesmo que o componente esteja fechado, mas se você tentar executar uma animação no componente, ela falhará. Nesse caso, uma lógica condicional extra dentro da assinatura seria a solução - e você NÃO deseja cancelar a assinatura do http observável.

Portanto, em resposta à pergunta real, não, você não precisa fazer isso para evitar vazamentos de memória. Mas você precisa fazer isso (frequentemente) para evitar que efeitos colaterais indesejados sejam acionados executando um código que pode gerar exceções ou corromper o estado do aplicativo.

Dica: O Subscriptioncontém uma closedpropriedade booleana que pode ser útil em casos avançados. Para HTTP, isso será definido quando for concluído. Em Angular, pode ser útil em algumas situações definir uma _isDestroyedpropriedade na ngDestroyqual possa ser verificada pelo seu subscribemanipulador.

Dica 2: Se estiver lidando com várias assinaturas, você poderá criar um new Subscription()objeto ad-hoc e add(...)quaisquer outras assinaturas - portanto, ao cancelar a assinatura do principal, ele também cancelará a assinatura de todas as assinaturas adicionadas.


2
Observe também que, se você tiver um serviço que retorne o http bruto passível de sobrescrita e o canalize antes de assinar, precisará cancelar a inscrição apenas do observável final, não do observável http subjacente. Na verdade, você nem terá uma assinatura do http diretamente, portanto não poderá.
Simon_Weaver

Embora o cancelamento da inscrição cancele a solicitação do navegador, o servidor ainda age na solicitação. Existe uma maneira de intimizar o servidor para abortar a operação também? Estou enviando solicitações http em rápida sucessão, das quais a maioria é cancelada por cancelar a inscrição. Mas, o servidor ainda opera com os pedidos cancelados pelo cliente, fazendo com que o pedido legítimo aguarde.
bala

@bala, você teria que criar seu próprio mecanismo para isso - o que não teria nada a ver com o RxJS. Por exemplo, você pode simplesmente colocar as solicitações em uma tabela e executá-las após 5 segundos em segundo plano; se algo precisar ser cancelado, você deve excluir ou definir um sinalizador nas linhas mais antigas que os impediriam de executar. Dependeria inteiramente de qual é a sua aplicação. No entanto, como você mencionou que o servidor está bloqueando, ele pode ser configurado para permitir apenas uma solicitação por vez - mas isso novamente dependeria do que você estava usando.
Simon_Weaver 21/01

2 Dica 2 - é um PRO-DICA, 🔥🔥🔥 para quem deseja cancelar a assinatura de várias assinaturas chamando apenas uma função. Adicione usando o método .add () e depois o .unsubscribe () no ngDestroy.
abhay tripathi

23

Chamar o unsubscribemétodo é, em vez disso, cancelar uma solicitação HTTP em andamento, pois esse método chama a que abortestá no objeto XHR subjacente e remove ouvintes nos eventos de carregamento e erro:

// From the XHRConnection class
return () => {
  _xhr.removeEventListener('load', onLoad);
  _xhr.removeEventListener('error', onError);
  _xhr.abort();
};

Dito isto, unsubscriberemove os ouvintes ... Portanto, pode ser uma boa ideia, mas não acho que seja necessário para uma única solicitação ;-)

Espero que ajude você, Thierry


: | isso é um absurdo: | Eu estava procurando uma maneira de parar ... mas fiquei imaginando, e se fosse eu codificando, em algum cenário: eu faria assim: y = x.subscribe((x)=>data = x); então uma mudança de usuário nas entradas e x.subscribe((x)=>cachlist[n] = x); y.unsubscribe()ei você usou nossos recursos de servidor, eu ganhei jogue você fora ..... e em outro cenário: basta chamar y.stop()jogar tudo fora
deadManN

16

Cancelar a inscrição é uma obrigação se você quiser um comportamento determinístico em todas as velocidades da rede.

Imagine que o componente A é renderizado em uma guia - Você clica em um botão para enviar uma solicitação 'GET'. Leva 200 ms para a resposta voltar. Portanto, você pode fechar a guia a qualquer momento sabendo que a máquina será mais rápida do que você e a resposta http será processada e concluída antes que a guia seja fechada e o componente A seja destruído.

Que tal uma rede muito lenta? Você clica em um botão, a solicitação 'GET' leva 10 segundos para receber sua resposta, mas, em 5 segundos, você decide fechar a guia. Isso destruirá o componente A para ser coletado no lixo posteriormente. Espere um minuto! , não cancelamos a inscrição - agora 5 segundos depois, uma resposta volta e a lógica no componente destruído será executada. Essa execução agora é considerada out-of-contexte pode resultar em várias coisas, incluindo desempenho muito baixo.

Portanto, a melhor prática é usar takeUntil()e cancelar a inscrição de chamadas http quando o componente é destruído.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

interface User {
  id: string;
  name: string;
  age: number;
}

@Component({
  selector: 'app-foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
})
export class FoobarComponent implements OnInit, OnDestroy {
  private user: User = null;
  private destroy$ = new Subject();

  constructor(private http: HttpClient) {}

  ngOnInit() {
    this.http
      .get<User>('api/user/id')
      .pipe(takeUntil(this.destroy$))
      .subscribe(user => {
        this.user = user;
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();  // trigger the unsubscribe
    this.destroy$.complete(); // finalize & clean up the subject stream
  }
}

Está tudo bem para usar BehaviorSubject()em vez disso e chamar apenas complete()no ngOnDestroy()?
Saksham

Você ainda precisa cancelar a inscrição ... por que BehaviourSubjectseria diferente?
Ben Taliadoros

Não é necessário cancelar a assinatura dos observáveis ​​HttpClient, pois são observáveis ​​finitos, ou seja, eles são concluídos após a emissão de um valor.
Yatharth Varshney

De qualquer forma, você deve cancelar a inscrição, suponha que exista um interceptador para brindar erros, mas você já saiu da página que acionou o erro. Você verá uma mensagem de erro em outra página ... Pense também em uma página de verificação de integridade que chama todos os microsserviços ... e assim por diante
Washington Guedes

12

Também com o novo módulo HttpClient, permanece o mesmo comportamento pacotes / common / http / src / jsonp.ts


O código acima parece ser a partir de um teste de unidade, o que implica que, com HttpClient, você precisa fazer para .complete call () em seu próprio país ( github.com/angular/angular/blob/... )
CleverPatrick

Sim, você está certo, mas se você inspecionar ( github.com/angular/angular/blob/… ), verá o mesmo. Em relação à questão principal, se necessário cancelar explicitamente a inscrição? Na maioria dos casos, não, pois será tratado pelo próprio Angular. Pode ser um caso em que uma resposta longa pode ocorrer e você se mudou para outra rota ou talvez destruiu um componente; nesse caso, você tem a possibilidade de tentar acessar algo que não existe mais, gerando uma exceção.
Ricardo Martínez

8

Você não deve cancelar a assinatura de observáveis ​​que são concluídos automaticamente (por exemplo, Http, chamadas). Mas é necessário cancelar a inscrição de infinitos observáveis ​​como Observable.timer().


6

Depois de um tempo de teste, a leitura da documentação e o código fonte do HttpClient.

HttpClient: https://github.com/angular/angular/blob/master/packages/common/http/src/client.ts

HttpXhrBackend : https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts

HttpClientModule: https://indepth.dev/exploring-the-httpclientmodule-in-angular/

Universidade Angular: https://blog.angular-university.io/angular-http/

Esse tipo específico de Observáveis ​​são fluxos de valor único: se a solicitação HTTP for bem-sucedida, esses observáveis ​​emitirão apenas um valor e, em seguida, serão concluídos.

E a resposta para toda a edição de "eu preciso" cancelar a inscrição?

Depende. Chamada HTTP Memoryleaks não é um problema. Os problemas são a lógica nas suas funções de retorno de chamada.

Por exemplo: Roteamento ou Login.

Se a sua ligação for de login, você não precisará "cancelar a inscrição", mas precisará garantir que, se o Usuário sair da página, lide com a resposta adequadamente na ausência do usuário.


this.authorisationService
      .authorize(data.username, data.password)
      .subscribe((res: HttpResponse<object>) => {
          this.handleLoginResponse(res);
        },
        (error: HttpErrorResponse) => {
          this.messageService.error('Authentication failed');
        },
        () => {
          this.messageService.info('Login has completed');
        })

De irritante a perigoso

Agora, imagine, a rede está mais lenta que o normal, a chamada leva mais 5 segundos e o usuário sai da visualização de login e passa para uma "visualização de suporte".

O componente pode não estar ativo, mas a assinatura. No caso de uma resposta, o usuário será redirecionado repentinamente (dependendo da sua implementação handleResponse ()).

Isso não é bom.

Imagine também que o usuário saia do PC, acreditando que ainda não está logado. Mas você lógico faz logon no usuário, agora você tem um problema de segurança.

O que você pode fazer SEM cancelar a inscrição?

Faça sua ligação dependente do estado atual da exibição:

  public isActive = false;
  public ngOnInit(): void {
    this.isActive = true;
  }

  public ngOnDestroy(): void {
    this.isActive = false;
  }

Usuário .pipe(takeWhile(value => this.isActive))para garantir que a resposta seja tratada apenas quando a exibição estiver ativa.


this.authorisationService
      .authorize(data.username, data.password).pipe(takeWhile(value => this.isActive))
      .subscribe((res: HttpResponse<object>) => {
          this.handleLoginResponse(res);
        },
        (error: HttpErrorResponse) => {
          this.messageService.error('Authentication failed');
        },
        () => {
          this.messageService.info('Login has completed');
        })

Mas como você pode ter certeza de que a assinatura não está causando falta de memória?

Você pode registrar se o "teardownLogic" for aplicado.

O teardownLogic de uma assinatura será chamado quando a subscrição estiver vazia ou sem assinatura.


this.authorisationService
      .authorize(data.username, data.password).pipe(takeWhile(value => this.isActive))
      .subscribe((res: HttpResponse<object>) => {
          this.handleLoginResponse(res);
        },
        (error: HttpErrorResponse) => {
          this.messageService.error('Authentication failed');
        },
        () => {
          this.messageService.info('Login has completed');
    }).add(() => {
        // this is the teardown function
        // will be called in the end
      this.messageService.info('Teardown');
    });

Você não precisa cancelar a inscrição. Você deve saber se há problemas em sua lógica que podem causar problemas em sua assinatura. E cuide deles. Na maioria dos casos, isso não será um problema, mas principalmente em tarefas críticas como a autorização, você deve cuidar de um comportamento inesperado, seja com "cancelamento de inscrição" ou com outra lógica, como funções de canal ou retorno de chamada condicional.

por que não sempre cancelar a inscrição?

Imagine que você faça uma solicitação de colocação ou publicação. O servidor recebe a mensagem de qualquer maneira, apenas a resposta demora um pouco. Cancelar a inscrição, não desfará a postagem nem a coloca. Porém, ao cancelar a inscrição, você não terá a chance de lidar com a resposta ou informar o Usuário, por exemplo, via Caixa de Diálogo ou um Toast / Message etc.

O que faz com que o usuário acredite que a solicitação de put / post não foi feita.

Então depende. É sua decisão de design, como lidar com esses problemas.


4

Você definitivamente deve ler este artigo. Ele mostra por que você deve sempre cancelar a inscrição, mesmo de http .

Se, após criar a solicitação, mas antes de receber uma resposta do back-end, você considerar o componente desnecessário e destruí-lo, sua assinatura manterá a referência ao componente, criando uma chance de causar vazamento de memória.

Atualizar

A afirmação acima parece verdadeira, mas de qualquer maneira, quando a resposta voltar, a assinatura http será destruída de qualquer maneira


1
Sim, e você verá um "cancelamento" vermelho na guia de rede do navegador, para ter certeza de que funciona.
Simon_Weaver 01/07/19

-2

O RxJS observável é basicamente associado e funciona de acordo com a sua assinatura. Quando criamos o observável e o movimento que o completamos, o observável é automaticamente fechado e cancelado.

Eles trabalham da mesma maneira que os observadores, mas com uma ordem bem diferente. A melhor prática para cancelar a inscrição quando o componente estiver sendo destruído. Poderíamos fazer isso posteriormente, por exemplo. this. $ manageSubscription.unsubscibe ()

Se criamos a sintaxe observável como abaixo mencionada, como

** retorna new Observable ((observador) => {** // Torna observável em estado frio ** observer.complete () **}) **

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.