Twój najlepszy przyjaciel, gdy masz do czynienia z wieloma obserwowalnymi strumieniami.

Nie ma wątpliwości, że RxJS jest obecnie jedną z najpopularniejszych bibliotek do tworzenia stron internetowych. Nie ma projektu, w którym biorę udział, w którym RxJS nie byłby używany i nie bez powodu. Zwłaszcza przy wykorzystaniu punktów integracji skierowanych do rosnącej liczby frameworków, bibliotek i narzędzi. Oferuje potężne i funkcjonalne podejście do radzenia sobie ze zdarzeniami. Dla mnie solidna znajomość RxJS i tego, co ma do zaoferowania, wydaje się oczywistością.

Nauka RxJS i programowania reaktywnego może być trudna, szczególnie jeśli dopiero zaczynasz tworzyć strony internetowe. Istnieje wiele nowych koncepcji, duża usługa API i nie zapominajmy o zasadniczej zmianie sposobu myślenia ze stylu imperatywnego na deklaratywny. Chociaż skupienie się na RxJ może być wyzwaniem, wiem, że było to dla mnie, i nie musi to być cholernie trudne.

W tym poście omówię jeden z najpopularniejszych operatorów transformacji, opiszę, czym jest operator transformacji i podam kilka przykładów, które można z nim połączyć

Czym są operatory transformacji?

Operatory transformacji, jak sama nazwa wskazuje, pozwalają przekształcać wartości w miarę ich przepływu przez obserwowalny strumień. W moim rozwoju operatory transformacji odegrały kluczową rolę w obsłudze żądań API i obsłudze przychodzących danych. Przekonasz się, że ci operatorzy oszczędzają dzień, więc nie musisz zajmować się wieloma zagnieżdżonymi subskrypcjami. Najpopularniejsze operatory transformacji należą do osobnej podsekcji i są znane jako operatory spłaszczające, takie jak: mergeMap, concatMap, switchMap i exhaustMap.

Co to jest operator spłaszczania?

Typowym przypadkiem użycia, z którym możesz się spotkać cały czas podczas tworzenia nowoczesnej aplikacji internetowej za pomocą RxJS, jest potrzeba emitowania z jednego strumienia do drugiego.

Wyobraźmy sobie, że mamy obserwowalny strumień z wprowadzonego tekstu i rejestrujemy dane wprowadzone przez użytkownika.

const textInput = document.getElementByID('search-box');
const input$ = fromEvent(textInput, 'keyup)
input$.pipe(
   map(event => event.target.value)
).subscribe(console.log)

Używamy operatora map do transformacji strumienia zdarzeń i zwrócenia tylko wartości docelowej. Jednak nadal emitujemy tylko podstawową wartość. W rzeczywistej aplikacji jest całkiem prawdopodobne, że nie tylko rejestrowalibyśmy wartość, ale moglibyśmy inicjować żądanie sieciowe na podstawie danych wprowadzonych przez użytkownika. Kiedy dopiero zaczynałem, rozwiązałem ten problem, zagnieżdżając subskrypcje w następujący sposób:

const textInput = document.getElementByID('search-box');
const input$ = fromEvent(textInput, 'keyup)
input$.pipe(
   map(event => {
       const term = event.target.value;
       return ajax.getJSON(`https://api.github.com/users/${term}`)
      })
).subscribe(response => { 
     response.subscribe(console.log)
});

Chociaż to działa, a odpowiedź pomyślnie loguje się do konsoli, naprawdę nie jest to idealne rozwiązanie z kilku powodów.

Po pierwsze, zagnieżdżone subskrypcje mogą wyglądać dość brzydko, zmniejszają czytelność, a śledzenie i debugowanie może być uciążliwe. W rezultacie masz drugą zagnieżdżoną subskrypcję, którą musisz następnie wziąć pod uwagę.

Ponadto może być nawet konieczne przekształcenie tej odpowiedzi, co spowoduje konieczność radzenia sobie z obserwowalnym z jego własnymi operatorami zagnieżdżonymi w ramach subskrypcji zawierającej wiele obserwowalnych. Coś takiego:

input$.pipe(
   map(event => {
       const term = event.target.value;
       return ajax.getJSON(`https://api.github.com/users/${term}`)
      })
).subscribe(response => { 
      response.pipe(
         map(({login, id}) => { 
          return of(`Do not do this. This is bad.`);
         })
      ).subscribe(response2 => { 
                  response2.subscribe(console.log)
                 })
});

Możesz zobaczyć, jak zaczyna to powodować ból głowy.

To tutaj przychodzą nasi operatorzy spłaszczania, aby uratować sytuację. Podsumowując: biorą obserwowalny, który emituje obserwowalne i zwraca obserwowalny składający się tylko z wyemitowanych obserwowalnych wartości. Brzmi to bardzo zagmatwanie, gdy mówię to na głos, więc pozwól, że ci pokażę, zaczynając od najprostszego operatora spłaszczania: mergeAll.

input$.pipe(
   map(event => {
      const term = event.target.value;
      return ajax.getJSON(`https://api.github.com/users/${term}`)
   }), 
   mergeAll()
).subscribe(console.log);

Kiedy mergeAll otrzymuje obserwowalność ze źródła, subskrybuje ją wewnętrznie, emitując dowolny wynik na podstawie tej obserwowalności. W tym przypadku subskrybuje obserwowalny ajax i wyemituje odpowiedź w tym strumieniu. Nie musisz mieć zagnieżdżonej subskrypcji i zacznij tworzyć piekło oddzwaniania, zamiast tego po prostu użyj operatora mergeAll i pozwól mu zrobić to za Ciebie.

Zrozumienie operatora mergeMap

Najpopularniejszym operatorem spłaszczającym (moim zdaniem) musi być operator mergeMap. Weźmy przykład z góry i uporządkujmy go jeszcze bardziej. Zamiast więc używać map, zamiast mergeAll. Łączymy te dwa elementy i używamy mergeMap:

input$.pipe(
   mergeMap(event => {
      const term = event.target.value;
      return ajax.getJSON(`https://api.github.com/users/${term}`)
   }), 
).subscribe(console.log);

Teraz mamy dokładnie takie samo zachowanie jak poprzednio, ale z jednym operatorem zamiast dwóch. Jestem pewien, że teraz widzisz siłę operatorów transformacji i to, jak poprawiają one obserwowalne strumienie.

Przejdźmy do innego operatora, switchMap. Aby zrozumieć różnice między mergeMap i switchMap, zobacz, gdzie operator mergeMap może okazać się niewystarczający, a gdzie switchMap może być lepszym wyborem.

Zrozumienie operatora switchMap

Operator switchMap odwzorowuje każdą wartość na obserwowalną, a następnie spłaszcza tę obserwowalną. W przeciwieństwie do mergeMap, switchMap utrzymuje tylko jedną aktywną subskrypcję wewnętrzną na raz.

Oznacza to, że za każdym razem, gdy mapujemy nową wewnętrzną obserwację, poprzednia jest zakończona.

To, co sprawia, że ​​switchMap jest wyjątkowe, widać na następnej emitowanej wartości ze źródła, którą można zaobserwować. W tym momencie poprzednio aktywna obserwacja wewnętrzna zostanie ukończona i nastąpi przełączenie na następną zwróconą obserwację.

Aby zrozumieć, czym switchMap różni się od mergeMap, zacznijmy od nowego przykładu. W tym przykładzie mamy strumień zdarzeń kliknięcia, które przy każdym kliknięciu odwzorowujemy na wewnętrzny interwał.

const interval$ = interval(1000);
const click$ = fromEvent(document, 'click');
click$.pipe(
    mergeMap(() => interval$)
).subscribe(console.log)

Po pierwszym kliknięciu tworzony jest odstęp. Po drugim kliknięciu tworzony jest drugi interwał, podczas gdy pierwszy interwał pozostaje. Jeśli nastąpi kolejne kliknięcie; tworzony jest trzeci interwał. Konsola będzie wyglądać mniej więcej tak:

0 1 2 3 0 4 1 5 2 6 0 3 7 1

Teraz zamieńmy tutaj mergeMap na switchMap.

Zobaczysz, że zachowanie będzie inne.

Po pierwszym kliknięciu interwał zacznie działać podobnie jak mergeMap, ale po drugim kliknięciu zamiast tworzenia drugiej subskrypcji wewnętrznej interwał zostanie uruchomiony ponownie:

click$.pipe(
    switchMap(() => interval$)
).subscribe(console.log)
// outcome: 0 1 2 3 0 1 2 0 1 2 3

Ponowne uruchomienie nastąpi po każdym pominięciu obserwowalnego źródła (kliknięcie $), nowy interwał zostanie zmapowany, następnie subskrybowany, a następnie poprzednio aktywna subskrypcja zostanie zakończona.

Teraz, gdy lepiej rozumiemy zachowanie operatora switchMap, przyjrzyjmy się przykładowi z życia wziętego:

const searchInput = document.getElementByID('search-box');
const input$ = fromEvent(searchInput, 'keyup')
input$.pipe(
  debounceTime(200),
  pluck('target', 'value'),
  distinctUntilChanged()
).subscribe(console.log)

Podobnie jak w naszym pierwszym przykładzie, mamy kolejny strumień zdarzeń „keyup” z danych wejściowych wyszukiwania.

Najpierw odrzucamy strumień za pomocą operatora debounceTime, aby wyemitować najnowszą wartość dopiero po pauzie trwającej 200 ms. Następnie używamy operatora pluck, aby wybrać właściwość zagnieżdżenia wartości docelowej, a następnie stosujemy operator distinctUntilChanged(), aby mieć pewność, że emitowane będą tylko unikalne wartości. Tak więc, gdy użytkownik wpisze dane wejściowe wyszukiwania, po pauzie trwającej 200 ms lub dłużej, wartość zostanie wyemitowana do konsoli.

Dodajmy switchMap, aby zakończyć tę funkcjonalność:

input$.pipe(
  debounceTime(200),
  pluck('target', 'value'),
  distinctUntilChanged(), 
  switchMap(searchTerm => { 
     return ajax.getJSON(`http://api.som.org/${searchTerm}`)
  })
).subscribe(console.log)

Odzwierciedla to mapowanie dowolnego wyszukiwanego hasła, które otrzymamy z danych wejściowych, na obserwowalny ajax, spłaszczanie go za pomocą operatora switchMap, a następnie emitowanie odpowiedzi.

Słyszę, jak pytasz, po co używać switchMap zamiast mergeMap dla tej funkcjonalności?

Dzięki mergeMap otrzymasz wiele odpowiedzi na serwer, a odpowiedzi mogą wrócić w niewłaściwej kolejności, co będzie oznaczać słabą wydajność i nieoptymalne wrażenia użytkownika. Będziesz wykonywać wiele niepotrzebnych połączeń z serwerem 👎

Za pomocą operatora switchMap przełącza się na nową obserwację, anulując poprzednią obserwację wewnętrzną, włączając żądanie, jeśli zostanie wyemitowana zaktualizowana wartość.

Operator switchMap jest postrzegany jako najbezpieczniejszy domyślny sposób spłaszczania. Jest naprawdę przydatny w przypadku żądań HTTP, które można anulować i doskonale nadaje się do funkcji odpoczynku, wstrzymywania i wznawiania.

Referencje: Niektóre przykłady kodu zostały zaczerpnięte z kursu podstawowego RxJs wydanego przez Ultimate Courses.