Kiedy powinienem utworzyć nową subskrypcję na określony efekt uboczny?

10

W zeszłym tygodniu odpowiedziałem na pytanie RxJS, w którym wdałem się w dyskusję z innym członkiem społeczności na temat: „Czy powinienem utworzyć subskrypcję dla każdego konkretnego efektu ubocznego, czy powinienem ogólnie starać się minimalizować subskrypcje?” Chcę wiedzieć, jakiej metodologii należy użyć w kontekście podejścia opartego na pełnej reaktywności aplikacji lub kiedy przełączać się między nimi. Pomoże mi to, a może innym, uniknąć niepotrzebnych dyskusji.

Informacje o konfiguracji

  • Wszystkie przykłady są w TypeScript
  • Aby lepiej skupić się na pytaniach, nie trzeba używać cykli życia / konstruktorów do subskrypcji i zachować niepowiązane ramy
    • Wyobraź sobie: subskrypcje są dodawane w init konstruktora / cyklu życia
    • Wyobraź sobie: Rezygnacja z subskrypcji odbywa się w trakcie niszczenia cyklu życia

Co to jest efekt uboczny (próba kątowa)

  • Aktualizacja / wejście w interfejsie użytkownika (np. value$ | async)
  • Wyjście / upstream komponentu (np. @Output event = event$)
  • Interakcja między różnymi usługami w różnych hierarchiach

Przykładowy przypadek użycia:

  • Dwie funkcje: foo: () => void; bar: (arg: any) => void
  • Dwie obserwowalne źródła: http$: Observable<any>; click$: Observable<void>
  • foojest wywoływany po http$emisji i nie potrzebuje żadnej wartości
  • barjest wywoływany po click$emisji, ale potrzebuje bieżącej wartościhttp$

Przypadek: utwórz subskrypcję dla każdego określonego efektu ubocznego

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

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

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

Przypadek: ogólnie minimalizuj subskrypcje

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

Krótko o mojej opinii

Rozumiem fakt, że subskrypcje na początku sprawiają, że krajobrazy Rx są bardziej złożone, ponieważ musisz na przykład pomyśleć o tym, w jaki sposób subskrybenci powinni wpływać na potok, czy nie (na przykład udostępniaj swoje obserwowalne lub nie). Ale im bardziej oddzielasz kod (tym bardziej się skupiasz: co się dzieje, kiedy), tym łatwiej jest utrzymać (testować, debugować, aktualizować) kod w przyszłości. Mając to na uwadze, zawsze tworzę pojedyncze obserwowalne źródło i pojedynczą subskrypcję dla dowolnego efektu ubocznego w moim kodzie. Jeśli dwa lub więcej efektów ubocznych, które mam, są wywoływane przez dokładnie to samo obserwowalne źródło, dzielę się moim obserwowalnym i subskrybuję każdy efekt uboczny indywidualnie, ponieważ może on mieć różne cykle życia.

Jonathan Stellwag
źródło

Odpowiedzi:

6

RxJS jest cennym zasobem do zarządzania operacjami asynchronicznymi i powinien być używany w celu uproszczenia kodu (w tym zmniejszenia liczby subskrypcji) tam, gdzie to możliwe. Podobnie po obserwowalnym nie powinna automatycznie następować subskrypcja tego obserwowalnego, jeśli RxJS zapewnia rozwiązanie, które może zmniejszyć całkowitą liczbę subskrypcji w Twojej aplikacji.

Istnieją jednak sytuacje, w których korzystne może być utworzenie subskrypcji, która nie jest absolutnie „konieczna”:

Przykładowy wyjątek - ponowne wykorzystanie materiałów obserwowalnych w jednym szablonie

Patrząc na twój pierwszy przykład:

// Component:

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

// Template:

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

Jeśli wartość $ zostanie użyta tylko raz w szablonie, skorzystałbym z potoku asynchronicznego i jego zalet dla oszczędności kodu i automatycznego anulowania subskrypcji. Jednak zgodnie z tą odpowiedzią należy unikać wielu odwołań do tej samej zmiennej asynchronicznej w szablonie, np .:

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

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

W tej sytuacji zamiast tego utworzę osobną subskrypcję i użyję jej do zaktualizowania zmiennej niesynchronicznej w moim komponencie:

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

Technicznie możliwe jest osiągnięcie tego samego rezultatu bez valueSub, ale wymagania aplikacji oznaczają, że jest to właściwy wybór.

Rozważenie roli i długości życia obserwowalnego przed podjęciem decyzji o subskrypcji

Jeśli dwa lub więcej obserwowalnych obiektów jest użytecznych tylko razem, należy użyć odpowiednich operatorów RxJS, aby połączyć je w jedną subskrypcję.

Podobnie, jeśli pierwsza () jest używana do filtrowania wszystkich oprócz pierwszej emisji obserwowalnego, myślę, że istnieje większy powód, aby być oszczędnym z kodem i unikać „dodatkowych” subskrypcji, niż obserwowalny, który odgrywa stałą rolę w sesja.

Tam, gdzie którykolwiek z obserwowalnych elementów jest użyteczny niezależnie od innych, warto rozważyć elastyczność i przejrzystość oddzielnych subskrypcji. Jednak zgodnie z moim wstępnym stwierdzeniem subskrypcja nie powinna być automatycznie tworzona dla każdego możliwego do zaobserwowania, chyba że istnieje wyraźny powód, aby to zrobić.

Odnośnie rezygnacji z subskrypcji:

Punktem przeciwko dodatkowym subskrypcjom jest to, że potrzeba więcej rezygnacji z subskrypcji . Jak już powiedziałeś, chcielibyśmy założyć, że wszystkie niezbędne subskrypcje są stosowane w Destroy, ale prawdziwe życie nie zawsze idzie tak gładko! Ponownie RxJS zapewnia przydatne narzędzia (np. First () ) w celu usprawnienia tego procesu, co upraszcza kod i zmniejsza ryzyko wycieków pamięci. Ten artykuł zawiera istotne dodatkowe informacje i przykłady, które mogą być cenne.

Osobiste preferencje / gadatliwość a zwięzłość:

Weź pod uwagę własne preferencje. Nie chcę odejść od ogólnej dyskusji na temat gadatliwości kodu, ale celem powinno być znalezienie właściwej równowagi między zbyt dużym „szumem” a nadmiernym zakodowaniem kodu. To może być warte obejrzenia .

Matt Saunders
źródło
Najpierw dziękuję za szczegółową odpowiedź! Odnośnie # 1: z mojego punktu widzenia potok asynchroniczny jest również efektem subskrypcji / ubocznym, tylko że jest zamaskowany wewnątrz dyrektywy. Jeśli chodzi o # 2, czy możesz dodać próbkę kodu, nie rozumiem, co dokładnie mi mówisz. Odnośnie subskrypcji: nigdy wcześniej nie miałem takiej subskrypcji, która musi być wypisana w innym miejscu niż ngOnDestroy. First () tak naprawdę domyślnie nie zarządza anulowaniem subskrypcji: 0 emituje = subskrypcja otwarta, chociaż komponent jest zniszczony.
Jonathan Stellwag
1
W punkcie 2 naprawdę chodziło o rozważenie roli każdego możliwego do zaobserwowania przy podejmowaniu decyzji, czy również założyć subskrypcję, zamiast automatycznego śledzenia każdego obserwowalnego z subskrypcją. W tym miejscu sugerowałbym, aby spojrzeć na medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87 , który mówi: „Utrzymywanie zbyt wielu obiektów subskrypcji to znak, że zarządzasz subskrypcjami koniecznie i nie czerpiesz korzyści o mocy Rx. ”
Matt Saunders
1
Doceniane dzięki @JonathanStellwag. Dziękujemy również za wywołania zwrotne informacji RE - czy w swoich aplikacjach wyraźnie wypisujesz się z każdej subskrypcji (nawet jeśli na przykład używana jest pierwsza ()), aby temu zapobiec?
Matt Saunders,
1
Tak. W bieżącym projekcie zminimalizowaliśmy około 30% wszystkich wiszących węzłów, po prostu zawsze rezygnując z subskrypcji.
Jonathan Stellwag
2
Preferuję rezygnację z subskrypcji takeUntilw połączeniu z funkcją wywoływaną z ngOnDestroy. Jest to jeden liner że dodaje to do rury: takeUntil(componentDestroyed(this)). stackoverflow.com/a/60223749/5367916
Kurt Hamilton
2

jeśli optymalizowanie subskrypcji to Twoja gra końcowa, dlaczego nie przejść do logicznej ekstremum i po prostu zastosować się do tego ogólnego wzorca:

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

Wyłączne wykonywanie efektów ubocznych za pomocą stuknięcia i aktywacji za pomocą scalania oznacza, że ​​masz tylko jedną subskrypcję.

Jednym z powodów, aby tego nie robić, jest kastrowanie tego, co czyni RxJS użytecznym. Jest to zdolność do komponowania obserwowalnych strumieni oraz subskrybowania / anulowania subskrypcji strumieni zgodnie z wymaganiami.

Twierdziłbym, że twoje obserwowalne powinny być logicznie skomponowane, a nie zanieczyszczone lub pomieszane w imię ograniczania subskrypcji. Czy efekt foo powinien być logicznie sprzężony z efektem paska? Czy jedno wymaga drugiego? Czy kiedykolwiek nie będę chciał uruchamiać foo, gdy emituje http $? Czy tworzę niepotrzebne sprzężenie między niepowiązanymi funkcjami? Są to wszystkie powody, dla których nie należy umieszczać ich w jednym strumieniu.

To wszystko nawet nie uwzględnia obsługi błędów, która jest łatwiejsza do zarządzania dzięki wielu subskrypcjom IMO

bryan60
źródło
Dziękuję za Twoją odpowiedź. Przykro mi, że mogę zaakceptować tylko jedną odpowiedź. Twoja odpowiedź jest taka sama, jak napisana przez @Matt Saunders. To tylko kolejny punkt widzenia. Dzięki wysiłkowi Matta wyraziłem zgodę. Mam nadzieję, że możesz mi wybaczyć :)
Jonathan Stellwag,