Quando devo criar uma nova assinatura para um efeito colateral específico?

10

Na semana passada, respondi a uma pergunta do RxJS em que entrei em uma discussão com outro membro da comunidade sobre: ​​"Devo criar uma assinatura para cada efeito colateral específico ou devo tentar minimizar as assinaturas em geral?" Quero saber qual metodologia usar em termos de uma abordagem de aplicativo totalmente reativo ou quando mudar de um para outro. Isso ajudará a mim e talvez a outros a evitar discussões não-cesarianas.

Informações de configuração

  • Todos os exemplos estão em TypeScript
  • Para um melhor foco na questão, não é necessário usar ciclos de vida / construtores para assinaturas e manter a estrutura independente
    • Imagine: As assinaturas são adicionadas no construtor / ciclo de vida init
    • Imagine: O cancelamento da inscrição é feito na destruição do ciclo de vida

O que é um efeito colateral (amostra angular)

  • Atualização / entrada na interface do usuário (por exemplo value$ | async)
  • Saída / montante de um componente (por exemplo @Output event = event$)
  • Interação entre diferentes serviços em diferentes hierarquias

Exemplo de uso:

  • Duas funções: foo: () => void; bar: (arg: any) => void
  • Duas fontes observáveis: http$: Observable<any>; click$: Observable<void>
  • fooé chamado depois de http$emitido e não precisa de valor
  • baré chamado após click$emissão, mas precisa do valor atual dehttp$

Caso: crie uma assinatura para cada efeito colateral específico

const foo$ = http$.pipe(
  mapTo(void 0)
);

const bar$ = http$.pipe(
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue)
  )
);

foo$.subscribe(foo);
bar$.subscribe(bar);

Caso: Minimizar assinaturas em geral

http$.pipe(
  tap(() => foo()),
  switchMap(httpValue => click$.pipe(
    mapTo(httpValue )
  )
).subscribe(bar);

Minha própria opinião em suma

Compreendo o fato de que as assinaturas tornam as paisagens do Rx mais complexas no início, porque você precisa pensar em como os assinantes devem afetar o canal ou não, por exemplo (compartilhe o que você observa ou não). Porém, quanto mais você separa seu código (mais se concentra: o que acontece quando), mais fácil é manter (testar, depurar, atualizar) seu código no futuro. Com isso em mente, eu sempre crio uma única fonte observável e uma única assinatura para qualquer efeito colateral no meu código. Se dois ou mais efeitos colaterais que eu tenho são acionados pela mesma fonte observável, compartilhamos meu observável e assino cada efeito colateral individualmente, porque ele pode ter ciclos de vida diferentes.

Jonathan Stellwag
fonte

Respostas:

6

O RxJS é um recurso valioso para gerenciar operações assíncronas e deve ser usado para simplificar seu código (incluindo a redução do número de assinaturas) sempre que possível. Da mesma forma, um observável não deve ser seguido automaticamente por uma assinatura desse observável, se o RxJS fornecer uma solução que possa reduzir o número geral de assinaturas em seu aplicativo.

No entanto, há situações em que pode ser benéfico criar uma assinatura que não seja estritamente 'necessária':

Uma exceção de exemplo - reutilização de observáveis ​​em um único modelo

Olhando para o seu primeiro exemplo:

// Component:

this.value$ = this.store$.pipe(select(selectValue));

// Template:

<div>{{value$ | async}}</div>

Se o valor $ for usado apenas uma vez em um modelo, eu aproveitaria o canal assíncrono e seus benefícios para economia de código e cancelamento automático da assinatura. No entanto, de acordo com esta resposta , várias referências à mesma variável assíncrona em um modelo devem ser evitadas, por exemplo:

// It works, but don't do this...

<ul *ngIf="value$ | async">
    <li *ngFor="let val of value$ | async">{{val}}</li>
</ul>

Nessa situação, eu criaria uma assinatura separada e a utilizaria para atualizar uma variável não assíncrona no meu componente:

// Component

valueSub: Subscription;
value: number[];

ngOnInit() {
    this.valueSub = this.store$.pipe(select(selectValue)).subscribe(response => this.value = response);
}

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

// Template

<ul *ngIf="value">
    <li *ngFor="let val of value">{{val}}</li>
</ul>

Tecnicamente, é possível obter o mesmo resultado sem valueSub, mas os requisitos do aplicativo significam que esta é a escolha certa.

Considerando o papel e a vida útil de um observável antes de decidir se deseja assinar

Se dois ou mais observáveis ​​são úteis apenas quando considerados em conjunto, os operadores RxJS apropriados devem ser usados ​​para combiná-los em uma única assinatura.

Da mesma forma, se first () estiver sendo usado para filtrar tudo, exceto a primeira emissão de um observável, acho que há mais razões para ser econômico com seu código e evitar assinaturas 'extras' do que para um observável que tem um papel contínuo em a sessão.

Onde qualquer um dos observáveis ​​individuais for útil independentemente dos outros, vale a pena considerar a flexibilidade e a clareza de assinaturas separadas. Mas, de acordo com minha declaração inicial, uma assinatura não deve ser criada automaticamente para todos os observáveis, a menos que haja um motivo claro para isso.

Em relação a cancelamentos de inscrição:

Um ponto contra assinaturas adicionais é que mais cancelamentos são necessários. Como você disse, gostaríamos de supor que todas as desinscrições necessárias são aplicadas no Destroy, mas a vida real nem sempre é assim! Novamente, o RxJS fornece ferramentas úteis (por exemplo, first () ) para otimizar esse processo, o que simplifica o código e reduz o potencial de vazamento de memória. Este artigo fornece informações e exemplos adicionais relevantes, que podem ser úteis.

Preferência / verbosidade pessoal vs. concisão:

Leve suas próprias preferências em consideração. Não quero me desviar para uma discussão geral sobre a verbosidade do código, mas o objetivo deve ser encontrar o equilíbrio certo entre muito "ruído" e tornar seu código excessivamente enigmático. Vale a pena dar uma olhada .

Matt Saunders
fonte
Primeiro, obrigado pela sua resposta detalhada! Em relação ao item 1: do meu ponto de vista, o canal assíncrono também é um efeito secundário de assinaturas / apenas que está oculto dentro de uma diretiva. Em relação ao item 2, você pode adicionar um exemplo de código, não entendo exatamente o que você está me dizendo exatamente. Com relação a cancelamentos de assinatura: nunca tive antes que assinaturas precisem ser canceladas em outro local que não seja o ngOnDestroy. O First () realmente não gerencia o cancelamento de inscrição para você por padrão: 0 emits = assinatura aberta, embora o componente esteja destruído.
Jonathan Stellwag
11
O ponto 2 era realmente apenas considerar o papel de cada observável ao decidir se também deveria criar uma assinatura, em vez de seguir automaticamente todos os observáveis ​​com uma assinatura. Nesse ponto, eu sugiro olhar para medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 , que diz: "Manter muitos objetos de assinatura por perto é um sinal de que você está gerenciando imperativamente suas assinaturas e não aproveitando as vantagens do poder de Rx ".
Matt Saunders
11
Obrigado, obrigado @JonathanStellwag. Obrigado também pelas informações de retorno de chamada RE - em seus aplicativos, você cancela explicitamente a assinatura de todas as assinaturas (mesmo onde a primeira () é usada, por exemplo), para evitar isso?
Matt Saunders
11
Sim eu quero. No projeto atual, minimizamos cerca de 30% de todos os nós por aí, apenas cancelando a inscrição sempre.
Jonathan Stellwag
2
Minha preferência por cancelar a inscrição é takeUntilcombinada com uma função chamada de ngOnDestroy. É um um forro que adiciona este ao tubo: takeUntil(componentDestroyed(this)). stackoverflow.com/a/60223749/5367916
Kurt Hamilton
2

se a otimização de assinaturas é o seu fim de jogo, por que não ir ao extremo lógico e seguir este padrão geral:

 const obs1$ = src1$.pipe(tap(effect1))
 const obs2$ = src2$pipe(tap(effect2))
 merge(obs1$, obs2$).subscribe()

A execução exclusiva de efeitos colaterais ao tocar e ativar com mesclagem significa que você só tem uma assinatura.

Uma razão para não fazer isso é que você está castrando muito do que torna o RxJS útil. Qual é a capacidade de compor fluxos observáveis ​​e assinar / cancelar a assinatura dos fluxos, conforme necessário.

Eu diria que seus observáveis ​​devem ser compostos logicamente e não poluídos ou confusos em nome da redução de assinaturas. O efeito foo deve ser logicamente associado ao efeito bar? Um precisa do outro? Será que algum dia eu não vou querer ativar gatilho quando o http $ for emitido? Estou criando acoplamentos desnecessários entre funções não relacionadas? Todas essas são razões para evitar colocá-las em um fluxo.

Isso nem sequer considera a manipulação de erros, que é mais fácil de gerenciar com várias assinaturas IMO

bryan60
fonte
Obrigado pela sua resposta. Lamento apenas poder aceitar uma resposta. Sua resposta é a mesma que a escrita por @Matt Saunders. É apenas mais um ponto de vista. Por causa do esforço de Matt, eu aceitei. Espero que você possa me perdoar :)
Jonathan Stellwag 25/02