Quando devo creare un nuovo abbonamento per un effetto collaterale specifico?


10

La scorsa settimana ho risposto a una domanda su RxJS in cui ho avuto una discussione con un altro membro della comunità in merito a: "Devo creare un abbonamento per ogni specifico effetto collaterale o devo cercare di ridurre al minimo gli abbonamenti in generale?" Voglio sapere quale metologia usare in termini di un approccio applicativo completamente reattivo o quando passare da uno all'altro. Questo aiuterà me e forse altri ad evitare discussioni sconnesse.

Informazioni sulla configurazione

  • Tutti gli esempi sono in TypeScript
  • Per focalizzarsi meglio sulla domanda, non utilizzare cicli di vita / costruttori per gli abbonamenti e mantenere un framework non correlato
    • Immagina: le sottoscrizioni vengono aggiunte in init costruttore / ciclo di vita
    • Immagina: l'annullamento dell'iscrizione avviene nel ciclo di vita

Che cos'è un effetto collaterale (campione angolare)

  • Aggiornamento / input nell'interfaccia utente (ad es. value$ | async)
  • Output / upstream di un componente (ad es. @Output event = event$)
  • Interazione tra servizi diversi su gerarchie diverse

Caso d'uso esemplare:

  • Due funzioni: foo: () => void; bar: (arg: any) => void
  • Due fonti osservabili: http$: Observable<any>; click$: Observable<void>
  • fooviene chiamato dopo che http$è stato emesso e non ha bisogno di alcun valore
  • barviene chiamato dopo l' click$emissione, ma richiede il valore corrente dihttp$

Caso: crea un abbonamento per ogni specifico effetto collaterale

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

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

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

Caso: ridurre al minimo gli abbonamenti in generale

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

La mia opinione in breve

Posso capire il fatto che gli abbonamenti rendono i paesaggi Rx più complessi all'inizio, perché devi pensare a come gli abbonati dovrebbero influenzare la pipa o meno, ad esempio (condividi il tuo osservabile o meno). Ma più separi il tuo codice (più ti concentri: cosa succede quando) più facile è mantenere (testare, eseguire il debug, aggiornare) il tuo codice in futuro. Con questo in mente, creo sempre un'unica fonte osservabile e una singola sottoscrizione per qualsiasi effetto collaterale nel mio codice. Se due o più effetti collaterali che ho sono innescati dalla stessa identica fonte osservabile, quindi condivido il mio osservabile e sottoscrivo individualmente ogni effetto collaterale, perché può avere cicli di vita diversi.

Risposte:


6

RxJS è una risorsa preziosa per la gestione delle operazioni asincrone e dovrebbe essere utilizzata per semplificare il codice (inclusa la riduzione del numero di abbonamenti) ove possibile. Allo stesso modo, un osservabile non dovrebbe essere automaticamente seguito da un abbonamento a tale osservabile, se RxJS fornisce una soluzione che può ridurre il numero complessivo di abbonamenti nella tua applicazione.

Tuttavia, ci sono situazioni in cui può essere utile creare un abbonamento non strettamente "necessario":

Un'eccezione di esempio: riutilizzo di osservabili in un unico modello

Guardando il tuo primo esempio:

// Component:

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

// Template:

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

Se il valore $ viene utilizzato una sola volta in un modello, trarrebbe vantaggio dalla pipe asincrona e dai suoi vantaggi per l'economia del codice e la disdetta automatica. Tuttavia, secondo questa risposta , dovrebbero essere evitati più riferimenti alla stessa variabile asincrona in un modello, ad esempio:

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

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

In questa situazione, vorrei invece creare un abbonamento separato e utilizzarlo per aggiornare una variabile non asincrona nel mio 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, è possibile ottenere lo stesso risultato senza valueSub, ma i requisiti dell'applicazione indicano che questa è la scelta giusta.

Considerare il ruolo e la durata di un osservabile prima di decidere se iscriversi

Se due o più osservabili sono utili solo se presi insieme, gli operatori RxJS appropriati dovrebbero essere usati per combinarli in un singolo abbonamento.

Allo stesso modo, se first () viene utilizzato per filtrare tutto tranne la prima emissione di un osservabile, penso che ci sia una ragione in più per essere economici con il tuo codice ed evitare abbonamenti "extra", che per un osservabile che ha un ruolo continuo in la sessione.

Laddove uno dei singoli osservabili sia utile indipendentemente dagli altri, può essere utile considerare la flessibilità e la chiarezza delle sottoscrizioni separate. Ma secondo la mia dichiarazione iniziale, un abbonamento non dovrebbe essere creato automaticamente per ogni osservabile, a meno che non ci sia una ragione chiara per farlo.

Per quanto riguarda le cancellazioni:

Un punto contro ulteriori abbonamenti è che sono necessari più annullamenti . Come hai detto, vorremmo supporre che tutte le cancellazioni necessarie vengano applicate su Distruggi, ma la vita reale non sempre procede così senza intoppi! Ancora una volta, RxJS fornisce strumenti utili (ad esempio first () ) per semplificare questo processo, che semplifica il codice e riduce il potenziale di perdite di memoria. Questo articolo fornisce ulteriori informazioni ed esempi pertinenti, che possono essere utili.

Preferenza / verbosità personali rispetto a terseness:

Prendi in considerazione le tue preferenze. Non voglio allontanarmi da una discussione generale sulla verbosità del codice, ma l'obiettivo dovrebbe essere quello di trovare il giusto equilibrio tra troppo "rumore" e rendere il tuo codice eccessivamente criptico. Potrebbe valere la pena dare un'occhiata .


Innanzitutto grazie per la risposta dettagliata! Per quanto riguarda il n. 1: dal mio punto di vista la pipa asincrona è anche un abbonamento / effetto collaterale, solo che è mascherato all'interno di una direttiva. Per quanto riguarda il numero 2, puoi aggiungere qualche esempio di codice, non capisco esattamente cosa mi stai dicendo. Per quanto riguarda le cancellazioni: non avevo mai avuto prima che le iscrizioni dovessero essere cancellate in qualsiasi altro posto diverso da ngOnDestroy. First () non gestisce davvero la cancellazione per impostazione predefinita: 0 emits = iscrizione aperta, sebbene il componente sia distrutto.
Jonathan Stellwag,

1
Il punto 2 riguardava davvero il ruolo di ciascun osservabile al momento di decidere se impostare anche un abbonamento, piuttosto che seguire automaticamente ogni osservabile con un abbonamento. A questo proposito, suggerirei di consultare medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 , che dice: "Mantenere troppi oggetti di abbonamento in giro è un segno che stai gestendo imperativamente i tuoi abbonamenti e non approfittarne del potere di Rx. "
Matt Saunders,

1
Apprezzato grazie @JonathanStellwag. Grazie anche per le informazioni sui callback RE - nelle tue app, annulli esplicitamente l'iscrizione a ogni abbonamento (anche dove viene utilizzato il primo (), per esempio), per impedirlo?
Matt Saunders,

1
Sì, certamente. Nell'attuale progetto abbiamo ridotto al minimo il 30% circa di tutti i nodi in giro annullando sempre la sottoscrizione.
Jonathan Stellwag il

2
La mia preferenza per l'annullamento dell'iscrizione è takeUntilin combinazione con una funzione chiamata da ngOnDestroy. È un uno di linea che aggiunge questo al tubo: takeUntil(componentDestroyed(this)). stackoverflow.com/a/60223749/5367916
Kurt Hamilton,

2

se l'ottimizzazione degli abbonamenti è il tuo endgame, perché non andare all'estremo logico e seguire semplicemente questo schema generale:

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

Eseguendo esclusivamente gli effetti collaterali alla spina e attivando con unione significa che hai sempre e solo un abbonamento.

Un motivo per non farlo è che stai sterilizzando gran parte di ciò che rende utile RxJS. Qual è la capacità di comporre flussi osservabili e iscriversi / annullare l'iscrizione ai flussi come richiesto.

Direi che i tuoi osservabili dovrebbero essere logicamente composti e non inquinati o confusi in nome della riduzione degli abbonamenti. L'effetto foo dovrebbe logicamente essere associato all'effetto bar? L'uno ha bisogno dell'altro? Potrò mai non volere trigger foo quando http $ emette? Sto creando un accoppiamento non necessario tra funzioni non correlate? Questi sono tutti motivi per evitare di metterli in un flusso.

Tutto ciò non sta nemmeno considerando la gestione degli errori, che è più facile da gestire con più abbonamenti IMO


La ringrazio per la risposta. Mi dispiace di poter accettare una sola risposta. La tua risposta è la stessa di quella scritta da @Matt Saunders. È solo un altro punto di vista. A causa dello sforzo di Matt, gli ho dato l'accettazione. Spero che tu possa perdonarmi :)
Jonathan Stellwag il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.