Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Cześć wszystkim. Siergiej Omelnicki jest w kontakcie. Niedawno prowadziłem transmisję na temat programowania reaktywnego, podczas której mówiłem o asynchronii w JavaScript. Dziś chciałbym zrobić notatki na temat tego materiału.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Zanim jednak zaczniemy od głównego materiału, musimy poczynić notatkę wprowadzającą. Zacznijmy więc od definicji: czym jest stos i kolejka?

Stos to kolekcja, której elementy pozyskiwane są metodą LIFO „ostatnie przyszło, pierwsze wyszło”.

Kolejka to kolekcja, której elementy pozyskiwane są według zasady FIFO „pierwsze weszło, pierwsze wyszło”.

OK, kontynuujmy.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

JavaScript jest jednowątkowym językiem programowania. Oznacza to, że istnieje tylko jeden wątek wykonania i jeden stos, na którym funkcje ustawiane są w kolejce do wykonania. Dlatego JavaScript może wykonywać tylko jedną operację na raz, podczas gdy inne operacje będą czekać na swoją kolej na stosie, aż zostaną wywołane.

Stos wywołań to struktura danych, która najprościej mówiąc rejestruje informację o miejscu w programie, w którym się znajdujemy. Jeśli przechodzimy do funkcji, wypychamy jej wpis na górę stosu. Kiedy wracamy z funkcji, wyjmujemy ze stosu najwyższy element i wracamy do miejsca, w którym wywołaliśmy funkcję. To wszystko, co może zrobić stos. A teraz niezwykle interesujące pytanie. Jak zatem działa asynchronia w JavaScript?

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Tak naprawdę oprócz stosu przeglądarki mają specjalną kolejkę do pracy z tzw. WebAPI. Funkcje w tej kolejce zostaną wykonane w kolejności dopiero po całkowitym wyczyszczeniu stosu. Dopiero potem są wypychane z kolejki na stos w celu wykonania. Jeśli w danej chwili na stosie znajduje się choć jeden element, to nie można go dodać do stosu. Właśnie z tego powodu wywoływanie funkcji po przekroczeniu limitu czasu często nie jest precyzyjne w czasie, ponieważ funkcja nie może przejść z kolejki na stos, gdy jest on pełny.

Przyjrzyjmy się poniższemu przykładowi i rozpocznijmy jego wykonanie krok po kroku. Zobaczmy też, co będzie się działo w systemie.

console.log('Hi');
setTimeout(function cb1() {
    console.log('cb1');
}, 5000);
console.log('Bye');

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

1) Jeszcze nic się nie dzieje. Konsola przeglądarki jest przejrzysta, stos wywołań jest pusty.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

2) Następnie do stosu wywołań dodawana jest komenda console.log('Hi').

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

3) I to się spełniło

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

4) Następnie console.log('Hi') jest usuwany ze stosu wywołań.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

5) Teraz przejdź do polecenia setTimeout(function cb1() {… }). Jest dodawany do stosu wywołań.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

6) Wykonywana jest komenda setTimeout(function cb1() {… }). Przeglądarka tworzy licznik czasu będący częścią internetowego interfejsu API. Przeprowadzi odliczanie.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

7) Komenda setTimeout(function cb1() {... }) zakończyła swoje działanie i jest usuwana ze stosu wywołań.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

8) Do stosu wywołań dodano polecenie console.log('Bye').

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

9) Wykonywana jest komenda console.log('Bye').

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

10) Polecenie console.log('Bye') zostaje usunięte ze stosu wywołań.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

11) Po upływie co najmniej 5000 ms licznik czasu kończy działanie i umieszcza wywołanie zwrotne cb1 w kolejce wywołania zwrotnego.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

12) Pętla zdarzeń pobiera funkcję cb1 z kolejki wywołań zwrotnych i umieszcza ją na stosie wywołań.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

13) Wykonywana jest funkcja cb1, która dodaje console.log('cb1') do stosu wywołań.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

14) Wykonywana jest komenda console.log('cb1').

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

15) Polecenie console.log('cb1') zostało usunięte ze stosu wywołań.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

16) Funkcja cb1 została usunięta ze stosu wywołań.

Spójrzmy na przykład z dynamiki:

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Cóż, przyjrzeliśmy się, jak asynchronia jest implementowana w JavaScript. Porozmawiajmy teraz krótko o ewolucji kodu asynchronicznego.

Ewolucja kodu asynchronicznego.

a(function (resultsFromA) {
    b(resultsFromA, function (resultsFromB) {
        c(resultsFromB, function (resultsFromC) {
            d(resultsFromC, function (resultsFromD) {
                e(resultsFromD, function (resultsFromE) {
                    f(resultsFromE, function (resultsFromF) {
                        console.log(resultsFromF);
                    })
                })
            })
        })
    })
});

Programowanie asynchroniczne, jakie znamy w JavaScript, można wdrożyć tylko za pomocą funkcji. Można je przekazywać, jak każdą inną zmienną, do innych funkcji. Tak narodziły się callbacki. I jest fajnie, zabawnie i zabawnie, dopóki nie zmieni się w smutek, melancholię i smutek. Dlaczego? To proste:

  • Wraz ze wzrostem złożoności kodu projekt szybko zamienia się w niejasne, wielokrotnie zagnieżdżone bloki – „piekło wywołań zwrotnych”.
  • Obsługa błędów może być łatwo przeoczona.
  • Nie można zwracać wyrażeń za pomocą return.

Wraz z pojawieniem się Promise sytuacja nieco się poprawiła.

new Promise(function(resolve, reject) {
    setTimeout(() => resolve(1), 2000);

}).then((result) => {
    alert(result);
    return result + 2;

}).then((result) => {
    throw new Error('FAILED HERE');
    alert(result);
    return result + 2;

}).then((result) => {
    alert(result);
    return result + 2;

}).catch((e) => {
    console.log('error: ', e);
});

  • Pojawiły się łańcuchy obietnic, które poprawiły czytelność kodu
  • Pojawiła się osobna metoda wyłapywania błędów
  • Dodano możliwość równoległego wykonania przy użyciu Promise.all
  • Zagnieżdżoną asynchronię możemy rozwiązać za pomocą async/await

Ale obietnice mają swoje ograniczenia. Nie da się np. odwołać obietnicy bez tańca z tamburynem, a co najważniejsze, działa ona w oparciu o jedną wartość.

Cóż, płynnie podeszliśmy do programowania reaktywnego. Zmęczony? No cóż, na szczęście możesz iść zrobić sobie herbatę, pomyśleć o tym i wrócić poczytać więcej. I będę kontynuować.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Programowanie reaktywne to paradygmat programowania skupiający się na przepływie danych i propagacji zmian. Przyjrzyjmy się bliżej, czym jest strumień danych.

// Получаем ссылку на элемент
const input = ducument.querySelector('input');

const eventsArray = [];

// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
    event => eventsArray.push(event)
);

Wyobraźmy sobie, że mamy pole wejściowe. Tworzymy tablicę i dla każdego zdarzenia wejściowego będziemy przechowywać zdarzenie w naszej tablicy. Jednocześnie zaznaczę, że nasza tablica jest posortowana według czasu, tj. indeks wydarzeń późniejszych jest większy niż indeks wydarzeń wcześniejszych. Taka tablica jest uproszczonym modelem przepływu danych, ale nie jest jeszcze przepływem. Aby tę tablicę można było bezpiecznie nazwać strumieniem, musi ona być w stanie w jakiś sposób poinformować abonentów, że napłynęły do ​​niej nowe dane. W ten sposób dochodzimy do definicji przepływu.

Strumień danych

const { interval } = Rx;
const { take } = RxOperators;

interval(1000).pipe(
    take(4)
)

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Przepływ to tablica danych posortowanych według czasu, która może wskazywać, że dane uległy zmianie. Teraz wyobraź sobie, jak wygodne staje się pisanie kodu, w którym jedna akcja wymaga wywołania kilku zdarzeń w różnych częściach kodu. Po prostu subskrybujemy strumień, a on powiadomi nas, gdy nastąpią zmiany. A biblioteka RxJs może to zrobić.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

RxJS to biblioteka do pracy z programami asynchronicznymi i opartymi na zdarzeniach, wykorzystujących obserwowalne sekwencje. Biblioteka udostępnia typ podstawowy Zauważalny, kilka typów pomocniczych (Obserwator, planista, podmiot) i operatory do pracy ze zdarzeniami jak z kolekcjami (mapuj, filtruj, redukuj, co i podobne z JavaScript Array).

Rozumiemy podstawowe pojęcia tej biblioteki.

Obserwowalny, obserwator, producent

Obserwowalny to pierwszy podstawowy typ, któremu się przyjrzymy. Ta klasa zawiera główną część implementacji RxJs. Jest powiązany z obserwowalnym strumieniem, który można subskrybować metodą subskrypcji.

Observable implementuje mechanizm pomocniczy do tworzenia aktualizacji, tzw Obserwator. Źródłem wartości dla Obserwatora jest tzw Producent. Może to być tablica, iterator, gniazdo internetowe, jakiś rodzaj zdarzenia itp. Można więc powiedzieć, że obserwowalny jest przewodnikiem pomiędzy Producentem a Obserwatorem.

Observable obsługuje trzy typy zdarzeń Observer:

  • dalej – nowe dane
  • error – błąd jeśli sekwencja zakończyła się z powodu wyjątku. zdarzenie to oznacza również zakończenie sekwencji.
  • kompletna — sygnał o zakończeniu sekwencji. Oznacza to, że nowych danych nie będzie już więcej.

Zobaczmy demo:

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Na początku będziemy przetwarzać wartości 1, 2, 3 i po 1 sekundzie. dostaniemy 4 i zakończymy nasz strumień.

Myśleć na głos

I wtedy zdałem sobie sprawę, że opowiadanie o tym jest ciekawsze niż pisanie o tym. 😀

Subskrypcja

Kiedy subskrybujemy strumień, tworzymy nową klasę subskrypcjaco daje nam możliwość wypisania się przy użyciu tej metody wypisać. Za pomocą tej metody możemy także grupować subskrypcje Dodaj. Cóż, logiczne jest, że możemy rozgrupować wątki za pomocą usunąć. Metody dodawania i usuwania akceptują inną subskrypcję jako dane wejściowe. Pragnę zauważyć, że wypisując się, wypisujemy wszystkie subskrypcje podrzędne tak, jakby wywołały metodę rezygnacji z subskrypcji. Zacząć robić.

Rodzaje strumieni

HOT
ZIMNO

Producent jest tworzony poza obserwowalnością
Producent jest tworzony wewnątrz obserwowalnego

Dane są przesyłane w momencie utworzenia obserwowalnego
Dane podawane są w momencie dokonywania subskrypcji

Potrzebujesz dodatkowej logiki rezygnacji z subskrypcji
Wątek sam się kończy

Używa relacji jeden do wielu
Używa relacji jeden do jednego

Wszystkie subskrypcje mają to samo znaczenie
Subskrypcje są niezależne

Dane mogą zostać utracone, jeśli nie masz subskrypcji
Ponownie wystawia wszystkie wartości strumieni dla nowej subskrypcji

Aby dać analogię, pomyślałbym o gorącym strumieniu jak o filmie w kinie. W którym momencie przybyłeś, od tego momentu zacząłeś oglądać. Porównałbym zimny przepływ do rozmowy z technikiem. wsparcie. Każdy rozmówca odsłuchuje nagranie poczty głosowej od początku do końca, ale możesz się rozłączyć, korzystając z opcji rezygnacji z subskrypcji.

Pragnę zauważyć, że istnieją także tzw. przepływy ciepłe (spotkałem się z tą definicją niezwykle rzadko i tylko w społecznościach zagranicznych) – jest to przepływ, który przechodzi z zimnego w gorący. Powstaje pytanie - gdzie użyć)) Podam przykład z praktyki.

Pracuję z Angularem. Aktywnie korzysta z rxjs. Aby otrzymać dane na serwer oczekuję zimnego wątku i użyję tego wątku w szablonie za pomocą asyncPipe. Jeśli użyję tego potoku kilka razy, to wracając do definicji zimnego strumienia, każdy potok będzie żądał danych od serwera, co jest co najmniej dziwne. A jeśli zamienię zimny strumień na ciepły, żądanie nastąpi raz.

Ogólnie rzecz biorąc, zrozumienie rodzaju przepływów jest dość trudne dla początkujących, ale ważne.

Operatorzy

return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
    .pipe(
        tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
        map(({ data }: TradeCompanyList) => data)
    );

Operatorzy zapewniają nam możliwość rozszerzenia możliwości pracy ze strumieniami. Pomagają kontrolować zdarzenia zachodzące w Obserwowalnym. Przyjrzymy się kilku najpopularniejszym, a więcej szczegółów na temat operatorów można znaleźć, korzystając z linków w przydatnych informacjach.

Operatorzy - z

Zacznijmy od operatora pomocniczego. Tworzy Observable w oparciu o prostą wartość.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operatory - filtr

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operator filtra, jak sama nazwa wskazuje, filtruje sygnał strumienia. Jeżeli operator zwróci wartość true, następuje pominięcie dalej.

Operatorzy - weź

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

take — Przyjmuje wartość liczby emiterów, po której wątek się kończy.

Operatory - debounceTime

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

debounceTime – odrzuca wyemitowane wartości, które mieszczą się w określonym przedziale czasu pomiędzy wyjściami – po upływie tego przedziału czasu emituje ostatnią wartość.

const { Observable } = Rx;
const { debounceTime, take } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next(i++)
  }, 1000);

 // Испускаем значение раз в 1500мс
  setInterval(() => {
    observer.next(i++)
  }, 1500);
}).pipe(
  debounceTime(700),  // Ожидаем 700мс значения прежде чем обработать
  take(3)
);  

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operatory - takeWhile

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Emituje wartości do momentu, aż takeWhile zwróci false, po czym wypisze się z wątku.

const { Observable } = Rx;
const { debounceTime, takeWhile } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next(i++)
  }, 1000);
}).pipe(
  takeWhile( producer =>  producer < 5 )
);  

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operatorzy - CombineLatest

Operator CombineLatest jest nieco podobny do operatora Promise.all. Łączy wiele wątków w jeden. Po tym jak każdy wątek wykona przynajmniej jedną emisję, otrzymujemy z każdego najnowsze wartości w postaci tablicy. Co więcej, po jakiejkolwiek emisji z połączonych strumieni, da to nowe wartości.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

const { combineLatest, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 750мс
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

combineLatest(observer_1, observer_2).pipe(take(5));

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operatorzy - zip

Zip — czeka na wartość z każdego wątku i tworzy tablicę na podstawie tych wartości. Jeśli wartość nie pochodzi z żadnego wątku, to grupa nie zostanie utworzona.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

const { zip, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 750
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 500
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
});

zip(observer_1, observer_2, observer_3).pipe(take(5));

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operatorzy - forkDołącz

forkJoin również łączy wątki, ale emituje wartość tylko wtedy, gdy wszystkie wątki są zakończone.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

const { forkJoin, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
}).pipe(take(3));

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 750
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
}).pipe(take(5));

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 500
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
}).pipe(take(4));

forkJoin(observer_1, observer_2, observer_3);

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operatorzy - mapa

Operator transformacji mapy przekształca wartość emitera na nową.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

const {  Observable } = Rx;
const { take, map } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next(i++);
  }, 1000);
}).pipe(
  map(x => x * 10),
  take(3)
);

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Operatorzy – udostępnij, dotknij

Operator kranu pozwala na wykonanie efektów ubocznych, czyli dowolnych akcji, które nie wpływają na sekwencję.

Operator udostępniania może zamienić zimny strumień w gorący.

Programowanie asynchroniczne w JavaScript (Callback, Promise, RxJs)

Skończyliśmy z operatorami. Przejdźmy do Tematu.

Myśleć na głos

A potem poszłam napić się herbaty. Mam dość tych przykładów 😀

Rodzina podmiotu

Omawiana rodzina jest doskonałym przykładem przepływów gorących. Klasy te stanowią swego rodzaju hybrydę, która pełni jednocześnie funkcję obserwatora i obserwatora. Ponieważ temat jest gorącym wątkiem, konieczne jest wypisanie się z niego. Jeśli mówimy o głównych metodach, są to:

  • dalej – przeniesienie nowych danych do strumienia
  • error – błąd i zakończenie wątku
  • Complete – zakończenie wątku
  • subskrybuj – subskrybuj strumień
  • unsubscribe – wypisanie się ze strumienia
  • asObservable – zamień się w obserwatora
  • toPromise – przekształca się w obietnicę

Jest 4 5 typów przedmiotów.

Myśleć na głos

Na streamie rozmawiały 4 osoby, ale okazało się, że dodały jeszcze jedną. Jak to mówią, żyj i ucz się.

Prosty temat new Subject()– najprostszy rodzaj przedmiotów. Utworzono bez parametrów. Przesyła wartości otrzymane dopiero po wykupieniu abonamentu.

Temat zachowania new BehaviorSubject( defaultData<T> ) – moim zdaniem najczęstszy rodzaj tematu. Dane wejściowe przyjmują wartość domyślną. Zawsze zapisuje dane ostatniego numeru, które są przesyłane podczas subskrypcji. Klasa ta posiada również przydatną metodę wartości, która zwraca bieżącą wartość strumienia.

Temat powtórzenia new ReplaySubject(bufferSize?: number, windowTime?: number) — Wejście może opcjonalnie przyjąć jako pierwszy argument wielkość bufora wartości, które będzie w sobie przechowywać, a jako drugi czas, w którym potrzebujemy zmian.

Temat asynchroniczny new AsyncSubject() — nic się nie dzieje podczas subskrypcji, a wartość zostanie zwrócona dopiero po zakończeniu. Zwrócona zostanie tylko ostatnia wartość strumienia.

Temat gniazda sieci Web new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) — Dokumentacja o nim milczy, a ja go widzę po raz pierwszy. Jeśli ktoś wie czym się zajmuje niech napisze a my to dodamy.

Uff. Cóż, omówiliśmy wszystko, co chciałem ci dzisiaj powiedzieć. Mam nadzieję, że ta informacja była przydatna. Z listą referencji możesz zapoznać się samodzielnie w zakładce przydatne informacje.

przydatne informacje

Źródło: www.habr.com

Dodaj komentarz