13 cze 2018
5 min

RxJS – share operator

Ciężko nie zauważyć, że budowanie aplikacji w Angular, łączy się z ciągłą pracą w bibliotece RxJS. Stwierdziłem, że od czasu do czasu, skrobnę art o strumieniach. Myślę również o jakimś artykule typu RxJS w pigułce, dla początkujących developerów Angulara. W pierwszym wpisie o RxJS, zapraszam Cię do zapoznania się z operatorem share().

Grupy operatorów

Zanim przejdę do analizy operatora share, przypomnimy sobie ogólny podział operatorów wraz z przykładami:

  • Filtrowanie:  filter, first, last, debounce, skipUntil…
  • Transformowanie: map, switchMap, scan, pluck…
  • Obsługa błędów: catchError, retry…
  • Tzw. utilsy, o różnym przeznaczeniu: tap, finally, toPromise…
  • Multicasting: publish, share, shareReplay…

Część operatorów jest dużo bardziej popularna od pozostałych, chociażby ciężko spotkać aplikację angularową, w której nie został wykorzystany operator map lub catchError. Być może o grupie multicasting słyszysz po raz pierwszy. Jeśli tak, to już spieszę się z wyjaśnieniem ;).

Operatory z grupy multicasting, pozwalają między innymi na współdzielenie efektów ubocznych przez wielu subskrybentów, konwertują również observable ze stanu cold na hot przy spełnieniu określonych warunków (domyślnie observable są cold z pewnymi wyjątkami, np. fromEvent(input, 'click’), który jest hot – polecam poczytać artykuł na ten temat – cold vs hot observables). Najłatwiej będzie nam to zrozumieć na przykładzie operatora share.

Share operator

Share pozwala na współdzielenie zasobów przez wielu subskrybentów.
Zobaczmy prosty przykład:

W powyższym przykładzie, skorzystaliśmy z operatora tap, który idealnie nadaje się do tzw. side-effects. Wywołujemy log, ale równie dobrze moglibyśmy tam przypisywać jakąś wartość lub wołać metodę klasy. W powyższym przykładzie, log z tap wywoła się dwukrotnie ponieważ mamy dwie subskrypcje do tego strumienia.  A co w przypadku w którym chcemy, aby dany side-effects wywołał się tylko raz? I tu właśnie z pomocą, przychodzi do nas operator share.

W powyższym przykładzie jest oczywiste, że nasz side-effect w postaci loga ’I want to be called only once’  wyświetli się dwa razy i faktycznie tak się dzieje, no bo przecież mamy użyte 2x subscribe na jednym strumieniu. Może być niefajnie, jeśli byłaby to bardziej znacząca funkcja z określoną logiką.

Załóżmy, że chcemy wyłącznie raz wywołać side-effect, mimo wielu subskrypcji. I tu właśnie z pomocą przychodzi operator share. Share zwraca nowy strumień, które współdzieli oryginalny strumień wejściowy. Strumień będzie współdzielony tak długo jak:

  • liczba subskrybentów będzie większa od 0
  • observable nie są „completed”

Jeśli spełnione są powyższe warunki, to strumień z operatorem share jest hot.

Zastosujmy zatem operator share i sprawdźmy, czy log wywoła się tylko raz:

Faktycznie, dzięki dodaniu operatora share:

this.user$ = this.http.get('https://jsonplaceholder.typicode.com/users/1')
   .pipe(tap(() => this.log()), share());

Nasz side-effect w postaci loga w operatorze tap, wywołał się wyłącznie raz.

Share i kod synchroniczny

Wyżej wspomniałem, że share będzie działał, tak długo jak subskrypcje nie są „completed”. Nasz log wywołał się raz, ponieważ nasz kod jest asynchroniczny. W momencie drugiego subscribe(), który odpalił się synchronicznie po pierwszym subscribe(), to pierwsza subskrypcja nie była jeszcze skompletowana (http.get nieco trwał).

Do czego dążę – jeśli Twój kod będzie w pełni synchroniczny, to share() nie zadziała w takiej postaci w jakiej byś przywidywał. Rozpatrzmy przykład:

Zamiast strzała do API, zastosowałem funkcję of, który zwraca mi observable synchronicznie. W momencie drugiej subskrypcji, pierwsza już się skompletowała (cały kod jest synchroniczny) i nie jest już zapewniony warunek, że jakakolwiek subskrypcja nie jest jeszcze „completed”. Stąd log z tap pojawił się dwa razy. W takiej sytuacji, musiałbym użyć operatora shareReplay() zamiast share(), aby log pojawił się wyłącznie raz.

Realne zastosowanie

W pracy korzystam z operatora share najczęściej w sytuacjach, gdzie na jednym widoku korzystam z wielu słowników, które dostarczają mi z backendu wartości np. do selectów w formularzach. Wygląda to mniej więcej tak:

dictionaries = this.dictionariesService
.getDictionaries([Dictionaries.POKEMONS, Dictionaries.HEROES, Dictionaries.CATS])
.pipe(share());

pokemons$ = this.dictionaries.pipe(map((data) => data[Dictionaries.POKEMONS]));
heroes$ = this.dictionaries.pipe(map((data) => data[Dictionaries.HEROES]));
cats$ = this.dictionaries.pipe(map((data) => data[Dictionaries.CATS]));
Następnie pokemons$, heroes$ i cats$ lądują w templatce wraz z async pipe. Dzięki temu, że share() zmienia observable na hot, to robię wyłącznie jeden request do backendu. Gdybym usunął share(), to każda subskrypcja do pokemons$, heroes$ i cats$, spodowałaby nowy strzał do serwera, a tego bym nie chciał :). Można by również this.dictionaries z powyższego przykładu od razu używać w templatce za pomocą *ngIf & as, a następnie siegać po słowniki po odpowiednich kluczach już w  templatce, ale to zależy od naszych preferencji.
Możesz sobie to sprawdzić w pierwszym przykładzie z artykułu w zakładce network w web toolsach, zrób 8x subscribe do user$ to przekonasz się, że poszło 8 zapytań do serwera,  a po użyciu share, mimo 8 subskrypcji, byłby tylko jeden strzał do BE.
Tyle na dzisiaj o RxJS, stay tuned!
Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.