Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Vsem pozdrav! Tukaj je Sergej Omelnitski. Nedavno sem imel prenos v živo o reaktivnem programiranju, kjer sem govoril o asinhronosti v JavaScriptu. Danes bi rad strnil to gradivo.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Preden začnemo s glavnim gradivom, potrebujemo uvod. Torej, začnimo z definicijami: kaj je sklad in kaj je vrsta?
Sklad je zbirka, katere elementi se obravnavajo po načelu 'zadnji vstopil, prvi izstopil' LIFO.
Vrsta je zbirka, katere elementi se obravnavajo po načelu 'prvi vstopil, prvi izstopil' FIFO.

Okej, nadaljujmo.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

JavaScript je enovreten programski jezik. To pomeni, da obstaja le en tok izvajanja in en sklad, v katerega se funkcije dodajajo v vrsto za izvajanje. Zato lahko JavaScript v enem trenutku izvede le eno operacijo, druge operacije pa čakajo na vrsto v skladu, dokler niso poklicane.

Sklad klicev je podatkovna struktura, ki, poenostavljeno povedano, zabeleži informacije o tem, kje smo v programu. Ko preidemo v funkcijo, v zgornji del sklada dodamo zapis o njej. Ko se iz funkcije vrnemo, vzamemo iz sklada najvišji element in se vrnemo tja, od koder smo klicali to funkcijo. To je vse, kar sklad zmore. In zdaj se pojavi zelo zanimivo vprašanje. Kako torej deluje asinhronost v JavaScriptu?

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Dejansko poleg sklada v brskalnikih obstaja posebna vrsta za delo s tako imenovanim WebAPI. Funkcije iz te vrste se izvršijo po vrstnem redu šele po tem, ko je sklad popolnoma prazen. Šele potem so premaknjene iz vrste v sklad za izvajanje. Če je v skladu trenutno vsaj en element, potem ne morejo priti v sklad. Prav zato so klici funkcij z zamudo pogosto časovno netočni, ker funkcija ne more priti iz vrste v sklad, dokler je ta poln.

Poglejmo naslednji primer in se ukvarjajmo z njegovim 'izvajanjem' korak za korakom. Poleg tega bomo pogledali, kaj se pri tem dogaja v sistemu.

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

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

1) Na začetku se nič ne dogaja. Konzola brskalnika je prazna, sklad klicev je prazen.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

2) Potem se ukaz console.log(‘Hi’) doda v sklad klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

3) In se izvrši.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

4) Potem se console.log(‘Hi’) odstrani iz sklada klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

5) Zdaj gremo na ukaz setTimeout(function cb1() {… }). Ta se doda v sklad klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

6) Ukaz setTimeout(function cb1() {… }) se izvrši. Brskalnik ustvari časovnik, ki je del Web API. Ta bo izvajal odštevanje časa.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

7) Ukaz setTimeout(function cb1() {… }) konča svoje delo in se odstrani iz sklada klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

8) Ukaz console.log(‘Bye’) se doda v sklad klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

9) Ukaz console.log(‘Bye’) se izvrši.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

10) Ukaz console.log(‘Bye’) se odstrani iz sklada klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

11) Ko mine vsaj 5000 ms, časovnik konča svoje delo in doda povratni klic cb1 v vrsto povratnih klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

12) Dogodkovna zanka vzame funkcijo cb1 iz vrste povratnih klicev in jo doda v sklad klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

13) Funkcija cb1 se izvrši in doda console.log(‘cb1’) v sklad klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

14) Ukaz console.log(‘cb1’) se izvrši.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

15) Ukaz console.log(‘cb1’) se odstrani iz sklada klicev.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

16) Funkcija cb1 se odstrani iz sklada klicev.

Poglejmo primer v dinamiki:

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Tako smo obravnavali, kako je asynchronicity izvedena v JavaScriptu. Zdaj pa hitro preidimo na evolucijo asinhronega kode.

Evolucija asinhronega kode.

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);
                    })
                })
            })
        })
    })
});

Asinhrono programiranje, kakršnega poznamo v JavaScriptu, lahko izvedemo samo z funkcijami. Te lahko predamo drugim funkcijam, kot katerokoli drugo spremenljivko. Tako so nastali povratni klici. In to je zabavno, dokler se ne spremeni v žalost, obup in žalost. Zakaj? Preprosto:

  • S povečevanjem zapletenosti kode se projekt hitro spremeni v malo razumljive večkratno utelešene bloke — "callback hell".
  • Obravnavo napak je enostavno spregledati.
  • Izrazov s return ni mogoče vračati.

S pojavom Promise se je stanje nekoliko izboljšalo.

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);
});

  • Pojavili so se verigi obljub, kar je izboljšalo berljivost kode.
  • Pojavil se je poseben način za zajemanje napak.
  • Pojavila se mogućnost paralelnog izvršavanja uz pomoć Promise.all
  • Ugniježdenu asinhronost možemo rešiti pomoću async/await

Ali obećanje ima svoja ograničenja. Na primer, obećanje se ne može otkazati bez dodatnih trikova, a najvažnije — radi sa jednom vrednošću.

Pa, polako smo došli do reaktivnog programiranja. Umorni ste? Pa dobro, možete da skuvate čaj, malo razmislite i vratite se da čitate dalje. A ja ću nastaviti.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Reaktivno programiranje— paradigma programiranja usmerena na tokove podataka i širenje promena. Hajde da detaljnije razjasnimo šta je to tok podataka.

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

const eventsArray = [];

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

Zamislimo da imamo polje za unos. Kreiramo niz, i na svaki keyup događaj input-a ćemo čuvati događaj u našem nizu. Pri tome, želeo bih da napomenem da je naš niz sortiran po vremenu — tj. indeks kasnijih događaja je veći od indeksa ranijih. Takav niz predstavlja pojednostavljeni model toka podataka, ali to još nije tok. Da bi se ovaj niz mogao smatrati tokom, on mora nekako obavestiti pretplatnike da su mu novi podaci dostupni. Tako smo došli do definicije toka.

Tok podataka

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

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

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Tok— to je niz podataka, sortiranih po vremenu, koji može obavestiti da su se podaci promenili. A sada zamislite koliko je zgodno pisati kod u kojem će jedno dejstvo zahtevati nekoliko događaja na različitim mestima u kodu. Jednostavno se pretplatimo na tok i on će nam sam javiti kada dođe do promena. I to može da uradi biblioteka RxJs.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

RxJS — to je biblioteka za rad sa asinhronim i događajima zasnovanim programima koristeći posmatrane nizove. Biblioteka pruža osnovni tip Observable, nekoliko pomoćnih tipova (Observer, Schedulers, Subjects) i operatore za rad sa događajima kao kolekcijama (map, filter, reduce, every i slične iz JavaScript Array).

Hajde da se upoznamo sa osnovnim pojmovima ove biblioteke.

Observable, Observer, Producer

Observable — prvi osnovni tip koji ćemo razmotriti. Ova klasa sadrži većinu implementacije RxJs. Povezana je sa posmatranim tokom na koji se može pretplatiti koristeći metodu subscribe.

U Observable se implementira pomoćni mehanizam za kreiranje ažuriranja, koji se naziva Observer. Izvor vrednosti za Observer naziva se Producer. To može biti niz, iterator, web socket, neki događaj itd. Tako da možemo reći da je observable posrednik između Producer i Observer.

Observable obrađuje tri vrste događaja Observer:

  • next – novi podaci
  • error – greška, ako je sekvenca okončana zbog izuzetne situacije. Ovaj događaj takođe podrazumeva završetak sekvence.
  • complete — signal o završetku sekvence. To znači da novih podataka više neće biti.

Pogledajmo demo:

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Na početku ćemo obraditi vrednosti 1, 2, 3, a nakon 1 sekunde dobićemo 4 i završiti naš tok.

Misli naglas

I tada sam shvatio da je pričanje bilo zanimljivije od pisanja o tome. 😀

Subscription

Kada se pretplatimo na tok, kreiramo novu klasu subscription, koja nam omogućava da otkažemo pretplatu koristeći metodu unsubscribe. Takođe možemo grupisati pretplate koristeći metodu add. Logično je da možemo razgrupisati tokove koristeći remove. Metodi add i remove uzimaju drugu pretplatu kao ulaz. Vredno je napomenuti da kada otkažemo pretplatu, otkazujemo se od svih podređenih pretplata kao da smo i njima pozvali metodu unsubscribe. Idemo dalje.

Vrste tokova

HOTCOLD
Producer se kreira spolja observable-aProducer se kreira unutar observable-a
Podaci se prenose u trenutku kreiranja observable-aPodaci se dostavljaju u trenutku pretplate
Potrebna je dodatna logika za otkazivanje pretplateTok se završava samostalno
Koristi vezu jedan-ka-višeKoristi vezu jedan-na-jedan
Sve pretplate imaju jedinstvenu vrednostPretplate su nezavisne
Podaci se mogu izgubiti ako nema pretplatePonovo objavljuje sve vrednosti toka za novu pretplatu

Če potegnemo analogijo, bi vroč tok predstavil kot film v kinu. Od trenutka, ko si prišel, začneš ogled. Hladen tok bi primerjal z klicem v tehnično podporo. Vsak, ki pokliče, posluša posnetek avtomatskega odgovora od začetka do konca, vendar lahko prekine klic s pomočjo unsubscribe.

Rad bi opozoril, da obstajajo tudi tako imenovani topli tokovi (to definicijo sem srečal zelo redko in le v tujih skupnostih) — to je tok, ki se transformira iz hladnega v vročega. Pojavi se vprašanje — kje ga uporabiti)) Prikazal bom primer iz prakse.

Delam z Angularem. Aktivno uporablja rxjs. Za pridobivanje podatkov na strežniku pričakujem hladen tok in ta tok uporabljam v predlogi s pomočjo asyncPipe. Če uporabljam to cev večkrat, se vrnem k definiciji hladnega toka, vsaka cev bo zahtevala podatke s strežnika, kar je milo rečeno nenavadno. Če pa spremenim hladen tok v topli, bo zahteva izvedena enkrat.

Dejansko je razumevanje vrst tokov precej zapleteno za začetnike, vendar je pomembno.

Operaterji

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

Možnosti za delo s tokovi nam ponujajo operaterji. Ti pomagajo nadzorovati dogodke, ki se odvijajo v Observable. Oglejali si bomo nekaj najbolj priljubljenih, podrobneje pa se lahko seznanite z operaterji prek povezav v koristnih informacijah.

Operaterji — of

Začnimo s pomožnim operaterjem of. Ustvari Observable na osnovi preproste vrednosti.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Operaterji — filter

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Operater filtriranja filter, kot lahko razumemo iz imena, filtrira signal toka. Če operater vrne resnico, ga naprej spusti.

Operaterji — take

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

take — sprejme število emitov, po katerem zaključi tok.

Operaterji — debounceTime

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

debounceTime — zavrže izpuščene vrednosti, ki pridejo v določenem časovnem okviru med izhodnimi podatki — po preteku časovnega intervala izpusti zadnjo vrednost.

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)
);  

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Операторы — takeWhile

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Излучает значения, пока takeWhile не вернет false, после чего отписывается от потока.

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 )
);  

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Операторы — combineLatest

Комбинированный оператор combineLatest похож на promise.all. Он объединяет несколько потоков в один. Как только каждый поток выполнит хотя бы одно излучение, мы получаем последние значения от каждого в виде массива. После любого нового излучения из объединённых потоков он будет возвращать новые значения.

Asinhrono programiranje v JavaScriptu. (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));

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Операторы — zip

Zip — ждёт значение из каждого потока и формирует массив на основе этих значений. Если значение не поступит из какого-либо потока, то группа не будет сформирована.

Asinhrono programiranje v JavaScriptu. (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));

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Операторы — forkJoin

forkJoin также объединяет потоки, но он излучает значение только когда все потоки завершены (complete).

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

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

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Emitting value every 1000ms
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
}).pipe(take(3));

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emitting value every 750
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
}).pipe(take(5));

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Emitting value every 500
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
}).pipe(take(4));

forkJoin(observer_1, observer_2, observer_3);

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Operatorji — map

Operator transformacije map pretvara emitovanu vrednost u novu.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

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

Observable.create((observer) => {
  let i = 1;
  // Emitting value every 1000ms
  setInterval(() => {
    observer.next(i++);
  }, 1000);
}).pipe(
  map(x => x * 10),
  take(3)
);

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Operatorji – share, tap

Operator tap omogućava izvođenje sporednih efekata, tj. bilo kakve radnje koje ne utiču na sekvencu.

Utility operator share može pretvoriti hladan tok u vruć.

Asinhrono programiranje v JavaScriptu. (Callback, Promise, RxJs)

Sa operatorima smo završili. Prelazimo na Subject.

Misli naglas

I onda sam otišao da pijem čaj. Umorili su me ovi primeri 😀

Familija subject-ova

Familija subject-ova je živopisan primer vrućih tokova. Ove klase su neka vrsta hibrida, koje deluju istovremeno kao observable i observer. Pošto je subject vrući tok, potrebno je odjaviti se od njega. Ako govorimo o osnovnim metodama, to su:

  • next – prenos novih podataka u tok
  • error – greška i završetak toka
  • complete – završetak toka
  • subscribe – pretplatiti se na tok
  • unsubscribe – odjaviti se od toka
  • asObservable – transformišemo u posmatrača
  • toPromise – transformiše u promise

Izdvajamo 4 do 5 tipova subject-ova.

Misli naglas

Na streamu sam govorio o 4, a ispostavilo se da su dodali još jedan. Kao što kažu, vek živi vek uči.

Običan Subject new Subject()– najjednostavnija vrsta subject-ova. Kreira se bez parametara. Prenosi vrednosti koje su stigle samo nakon pretplate.

BehaviorSubject new BehaviorSubject( defaultData ) – po mom mišljenju, najraširenija vrsta subject-ova. Prima vrednost po default-u. Uvek čuva podatke poslednjeg emita, koje prenosi pri pretplati. Ova klasa takođe ima korisnu metodu value, koja vraća trenutnu vrednost toka.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) — Na ulazu može opcionalno primiti prvi argument kao veličinu baferirajuće vrednosti koju će čuvati, a drugi kao vreme tokom kojeg su nam potrebne izmenjene vrednosti.

AsyncSubject novi AsyncSubject() — pri naročanju se nič ne zgodi, vrednost pa bo vrnjena le ob complete. Vrnjena bo le zadnja vrednost toka.

WebSocketSubject nov WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) — O njem dokumentacija molči in ga prvič vidim. Kdor ve, kaj počne, naj piše, dopolnimo.

Uff. No, pregledali smo vse, kar sem želel danes povedati. Upam, da so bile te informacije koristne. Seznam literature si lahko ogledate v zavihku uporabne informacije.

Uporabne informacije

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster