Wann sollte ich ein neues Abonnement für eine bestimmte Nebenwirkung erstellen?

10

Letzte Woche beantwortete ich eine RxJS- Frage, in der ich mit einem anderen Community-Mitglied über Folgendes diskutierte: "Soll ich für jede bestimmte Nebenwirkung ein Abonnement erstellen oder sollte ich versuchen, Abonnements im Allgemeinen zu minimieren?" Ich möchte wissen, welche Methologie im Hinblick auf einen vollständig reaktiven Anwendungsansatz zu verwenden ist oder wann von einer zur anderen gewechselt werden muss. Dies wird mir und vielleicht anderen helfen, unnötige Diskussionen zu vermeiden.

Setup-Informationen

  • Alle Beispiele sind in TypeScript
  • Zur besseren Fokussierung auf die Frage keine Verwendung von Lebenszyklen / Konstruktoren für Abonnements und um im Framework unabhängig zu bleiben
    • Stellen Sie sich vor: Abonnements werden in Konstruktor / Lebenszyklus-Init hinzugefügt
    • Stellen Sie sich vor: Das Abbestellen erfolgt im Lebenszyklus zerstören

Was ist eine Nebenwirkung (Winkelprobe)

  • Update / Eingabe in der Benutzeroberfläche (zB value$ | async)
  • Output / stromaufwärts einer Komponente (z @Output event = event$)
  • Interaktion zwischen verschiedenen Diensten in verschiedenen Hierarchien

Beispielhafter Anwendungsfall:

  • Zwei Funktionen: foo: () => void; bar: (arg: any) => void
  • Zwei beobachtbare Quellen: http$: Observable<any>; click$: Observable<void>
  • foowird aufgerufen, nachdem http$ausgegeben wurde und keinen Wert benötigt
  • barwird nach click$emits aufgerufen , benötigt aber den aktuellen Wert vonhttp$

Fall: Erstellen Sie ein Abonnement für jede bestimmte Nebenwirkung

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

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

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

Fall: Abonnements im Allgemeinen minimieren

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

Meine eigene Meinung kurz

Ich kann die Tatsache verstehen, dass Abonnements Rx-Landschaften zunächst komplexer machen, weil Sie sich überlegen müssen, wie Abonnenten die Pipe beeinflussen sollen oder nicht (teilen Sie Ihre beobachtbaren oder nicht). Aber je mehr Sie Ihren Code trennen (je mehr Sie sich konzentrieren: Was passiert wann), desto einfacher ist es, Ihren Code in Zukunft zu warten (zu testen, zu debuggen, zu aktualisieren). In diesem Sinne erstelle ich immer eine einzige beobachtbare Quelle und ein einziges Abonnement für alle Nebenwirkungen in meinem Code. Wenn zwei oder mehr Nebenwirkungen, die ich habe, durch genau dieselbe beobachtbare Quelle ausgelöst werden, teile ich meine beobachtbaren und teile jede Nebenwirkung einzeln, da sie unterschiedliche Lebenszyklen haben kann.

Jonathan Stellwag
quelle

Antworten:

6

RxJS ist eine wertvolle Ressource für die Verwaltung asynchroner Vorgänge und sollte verwendet werden, um Ihren Code nach Möglichkeit zu vereinfachen (einschließlich der Reduzierung der Anzahl der Abonnements). Ebenso sollte auf ein Observable nicht automatisch ein Abonnement für dieses Observable folgen, wenn RxJS eine Lösung bietet, mit der die Gesamtzahl der Abonnements in Ihrer Anwendung reduziert werden kann.

Es gibt jedoch Situationen, in denen es vorteilhaft sein kann, ein Abonnement zu erstellen, das nicht unbedingt erforderlich ist:

Eine Beispielausnahme - Wiederverwendung von Observablen in einer einzelnen Vorlage

Schauen Sie sich Ihr erstes Beispiel an:

// Component:

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

// Template:

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

Wenn der Wert $ in einer Vorlage nur einmal verwendet wird, würde ich die asynchrone Pipe und ihre Vorteile für die Codeökonomie und die automatische Abmeldung nutzen. Doch nach dieser Antwort sollten mehrere Verweise auf das gleiche Asynchron - Variable in einer Vorlage vermieden werden, zB:

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

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

In dieser Situation würde ich stattdessen ein separates Abonnement erstellen und damit eine nicht asynchrone Variable in meiner Komponente aktualisieren:

// 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>

Technisch ist es möglich, dasselbe Ergebnis ohne zu erzielen valueSub, aber die Anforderungen der Anwendung bedeuten, dass dies die richtige Wahl ist.

Berücksichtigen Sie die Rolle und Lebensdauer eines Observable, bevor Sie sich für ein Abonnement entscheiden

Wenn zwei oder mehr Observablen nur zusammen verwendet werden können, sollten die entsprechenden RxJS-Operatoren verwendet werden, um sie zu einem einzigen Abonnement zu kombinieren.

Wenn first () verwendet wird, um alle außer der ersten Emission eines Observablen herauszufiltern, gibt es meines Erachtens einen größeren Grund, mit Ihrem Code sparsam umzugehen und zusätzliche Abonnements zu vermeiden, als für ein Observable, das eine fortlaufende Rolle in spielt die Sitzung.

Wenn eine der einzelnen Observablen unabhängig von den anderen nützlich ist, kann die Flexibilität und Klarheit separater Abonnements in Betracht gezogen werden. Nach meiner ursprünglichen Aussage sollte jedoch nicht automatisch für jedes beobachtbare Objekt ein Abonnement erstellt werden, es sei denn, es gibt einen eindeutigen Grund dafür.

In Bezug auf Abmeldungen:

Ein Punkt gegen zusätzliche Abonnements ist, dass mehr Abmeldungen erforderlich sind. Wie Sie gesagt haben, möchten wir davon ausgehen, dass alle erforderlichen Abmeldungen auf Destroy angewendet werden, aber das wirkliche Leben verläuft nicht immer so reibungslos! Auch hier bietet RxJS nützliche Tools (z. B. first () ) zur Optimierung dieses Prozesses, die den Code vereinfachen und das Risiko von Speicherlecks verringern. Dieser Artikel enthält relevante weitere Informationen und Beispiele, die von Wert sein können.

Persönliche Präferenz / Ausführlichkeit vs. Knappheit:

Berücksichtigen Sie Ihre eigenen Vorlieben. Ich möchte nicht zu einer allgemeinen Diskussion über Code-Ausführlichkeit übergehen, aber das Ziel sollte darin bestehen, das richtige Gleichgewicht zwischen zu viel "Rauschen" und einer zu kryptischen Codierung Ihres Codes zu finden. Dies könnte einen Blick wert sein .

Matt Saunders
quelle
Zunächst vielen Dank für Ihre ausführliche Antwort! In Bezug auf # 1: Aus meiner Sicht ist die asynchrone Pipe auch ein Abonnement / Nebeneffekt, nur dass sie innerhalb einer Direktive maskiert ist. In Bezug auf # 2 können Sie bitte ein Codebeispiel hinzufügen. Ich verstehe nicht genau, was Sie mir genau sagen. In Bezug auf Abmeldungen: Ich hatte noch nie zuvor das Gefühl, dass Abonnements an einem anderen Ort als dem ngOnDestroy abgemeldet werden müssen. First () verwaltet das Abbestellen standardmäßig nicht wirklich für Sie: 0 emits = Abonnement geöffnet, obwohl die Komponente zerstört ist.
Jonathan Stellwag
1
Bei Punkt 2 ging es wirklich nur darum, die Rolle jedes Observablen bei der Entscheidung zu berücksichtigen, ob auch ein Abonnement eingerichtet werden soll, anstatt jedem Observable automatisch ein Abonnement zu folgen. In diesem Zusammenhang würde ich vorschlagen, dass Sie sich unter medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 umsehen : "Wenn Sie zu viele Abonnementobjekte behalten , ist dies ein Zeichen dafür, dass Sie Ihre Abonnements unbedingt verwalten und nicht ausnutzen." von der Kraft von Rx. "
Matt Saunders
1
Vielen Dank @JonathanStellwag. Vielen Dank auch für die Info-RE-Rückrufe. Melden Sie sich in Ihren Apps explizit von jedem Abonnement ab (auch wenn beispielsweise first () verwendet wird), um dies zu verhindern?
Matt Saunders
1
Ja, ich will. Im aktuellen Projekt haben wir etwa 30% aller herumhängenden Knoten minimiert, indem wir uns immer abgemeldet haben.
Jonathan Stellwag
2
Ich bevorzuge das Abbestellen takeUntilin Kombination mit einer Funktion, von der aus aufgerufen wird ngOnDestroy. Es ist ein Einzeiler, der dies dem Rohr hinzufügt : takeUntil(componentDestroyed(this)). stackoverflow.com/a/60223749/5367916
Kurt Hamilton
2

Wenn die Optimierung von Abonnements Ihr Endspiel ist, gehen Sie doch zum logischen Extrem und folgen Sie einfach diesem allgemeinen Muster:

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

Das ausschließliche Ausführen von Nebenwirkungen beim Tippen und Aktivieren mit Zusammenführen bedeutet, dass Sie immer nur ein Abonnement haben.

Ein Grund, dies nicht zu tun, ist, dass Sie viel von dem kastrieren, was RxJS nützlich macht. Welches ist die Fähigkeit, beobachtbare Streams zu erstellen und Streams nach Bedarf zu abonnieren / abbestellen.

Ich würde argumentieren, dass Ihre Observablen logisch zusammengesetzt und nicht im Namen der Reduzierung von Abonnements verschmutzt oder verwirrt sein sollten. Sollte der foo-Effekt logisch mit dem Balkeneffekt gekoppelt werden? Benötigt einer den anderen? Werde ich jemals möglicherweise nicht wollen, dass Foo ausgelöst wird, wenn http $ ausgegeben wird? Schaffe ich eine unnötige Kopplung zwischen nicht verwandten Funktionen? Dies sind alles Gründe, um zu vermeiden, dass sie in einem Stream zusammengefasst werden.

Dies alles berücksichtigt nicht einmal die Fehlerbehandlung, die mit IMO mit mehreren Abonnements einfacher zu verwalten ist

bryan60
quelle
Vielen Dank für Ihre Antwort. Es tut mir leid, dass ich nur eine Antwort akzeptieren kann. Ihre Antwort ist die gleiche wie die von @Matt Saunders. Es ist nur eine andere Sichtweise. Wegen Matts Bemühungen gab ich ihm die Annahme. Ich hoffe du kannst mir vergeben :)
Jonathan Stellwag