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

Pozdrav svima. Ovde je Sergej Omelnitzki. Nedavno sam imao stream o reaktivnom programiranju, gde sam pričao o asinhronosti u JavaScriptu. Danas bih želeo da sažmem ovaj materijal.

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

Ali pre nego što započnemo glavni materijal, potrebno je da uradimo uvod. Dakle, hajde da počnemo s definicijama: šta je stek i red?
Stek je kolekcija čiji se elementi uzimaju po principu „poslednji došao, prvi otišao“ LIFO
Red je kolekcija čiji se elementi uzimaju po principu „prvi došao, prvi otišao“ FIFO

U redu, nastavljamo.

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

JavaScript je jezgro za programiranje sa jednim tokom. To znači da postoji samo jedan tok izvršavanja i jedan stek, u koji se funkcije stavljaju u red za izvršavanje. Samim tim, u jednom trenutku JavaScript može izvršiti samo jednu operaciju, dok ostale operacije čekaju svoj red u steku, dok ne budu pozvane.

Stek poziva je struktura podataka koja, pojednostavljeno rečeno, beleži informacije o mestu u programu gde se nalazimo. Ako ulazimo u funkciju, smeštamo njen zapis na vrh steka. Kada se vraćamo iz funkcije, uzimamo najgornji element iz steka i vraćamo se na mesto odakle smo pozvali tu funkciju. To je sve što stek radi. A sada izuzetno zanimljivo pitanje. Kako onda radi asinhronost u JavaScriptu?

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

U stvari, pored steka, u browserima postoji poseban red za rad sa takozvanim WebAPI. Funkcije iz ovog reda biće izvršene redom tek nakon što se stek potpuno isprazni. Tek tada se prebacuju iz reda u stek za izvršavanje. Ako u steku trenutno postoji barem jedan element, one ne mogu doći u stek. Zbog toga, pozivi funkcija putem timeouta često nisu tačni u vremenu, jer funkcija ne može ući iz reda u stek dok je on pun.

Razmotrićemo sledeći primer i posvetiti se njegovom korak-po-korak „izvršenju“. Takođe, razmotrićemo šta se u tom trenutku dešava u sistemu.

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

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

1) Dok se ništa ne dešava. Konzola browsera je prazna, stek poziva je prazan.

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

2) Potom se komanda console.log('Hi') dodaje u stek poziva.

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

3) I ona se izvršava

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

4) Zatim se console.log(‘Hi’) uklanja iz stoga poziva.

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

5) Sada prelazimo na naredbu setTimeout(function cb1() {… }). Ona se dodaje u stog poziva.

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

6) Naredba setTimeout(function cb1() {… }) se izvršava. Preglednik kreira tajmer koji je deo Web API-ja. On će izvršiti odbrojavanje.

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

7) Naredba setTimeout(function cb1() {… }) je završila sa radom i uklanja se iz stoga poziva.

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

8) Naredba console.log(‘Bye’) se dodaje u stog poziva.

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

9) Naredba console.log(‘Bye’) se izvršava.

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

10) Naredba console.log(‘Bye’) se uklanja iz stoga poziva.

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

11) Nakon što prođe najmanje 5000 ms, tajmer završava sa radom i stavlja povratni poziv cb1 u red povratnih poziva.

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

12) Ciklus događaja uzima funkciju cb1 iz reda povratnih poziva i stavlja je u stog poziva.

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

13) Funkcija cb1 se izvršava i dodaje console.log(‘cb1’) u stog poziva.

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

14) Naredba console.log(‘cb1’) se izvršava.

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

15) Naredba console.log(‘cb1’) se uklanja iz stoga poziva.

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

16) Funkcija cb1 se uklanja iz stoga poziva.

Pogledajmo primer u dinamici:

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

Evo kako smo obradili asinkronost u JavaScript-u. Sada hajde da ukratko razgovaramo o evoluciji asinkronog koda.

Evolucija asinkronog koda.

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

Asinkrono programiranje, kakvo ga poznajemo u JavaScript-u, može se realizovati samo funkcijama. One se mogu preneti kao bilo koja druga promenljiva drugim funkcijama. Tako su rođeni povratni pozivi. I to je zabavno i veselo, sve dok se ne pretvori u tugu i jad. Zašto? Zato što:

  • Sa povećanjem složenosti koda, projekat brzo postaje teško razumljiv sa više nivoa ugnježdenih blokova — „ozbiljna tjeskoba sa povratnim pozivima“.
  • Obrada grešaka se lako može propustiti.
  • Ne možete vraćati izraze sa return.

Sa pojavom Promise situacija se malo poboljšala.

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

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

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

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

}).catch((e) => {
    console.log('greška: ', e);
});

  • Pojavili su se lanci proma, što je poboljšalo čitljivost koda
  • Pojavila se posebna metoda za hvatanje grešaka
  • Pojavila se mogućnost paralelnog izvršavanja pomoću Promise.all
  • Ugniježđenu asinkronost možemo riješiti pomoću async/await

Ali promisi imaju svoja ograničenja. Na primjer, niti jedan promisor se ne može otkazati bez dodatnog truda, a što je još važnije — radi sa jednom vrijednošću.

Pa, polako smo došli do reaktivnog programiranja. Umorni ste? Pa dobro, možete skuhati čaj, razmisliti i vratiti se čitanju. A ja ću nastaviti.

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

Reaktivno programiranje— paradigma programiranja usmjerena na tokove podataka i distribuciju promjena. Pogledajmo detaljnije što 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 unosa ćemo spremiti događaj u našem nizu. Također, važno je napomenuti da je naš niz sortirano prema 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 bismo ovaj niz mogli slobodno nazvati tokom, mora na neki način obavijestiti pretplatnike da su stigli novi podaci. Na taj način smo došli do definicije toka.

Tok podataka

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

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

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

Tok— to je niz podataka, sortiranih prema vremenu, koji može obavijestiti da su podaci promijenjeni. A sada zamislite koliko je zgodno pisati kod u kojem će na jedan događaj biti potrebno pozvati nekoliko događaja na različitim mjestima u kodu. Jednostavno se pretplatimo na tok i on će nas sam obavijestiti kada dođe do promjena. To može učiniti biblioteka RxJs.

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

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

Pogledajmo osnovne pojmove ove biblioteke.

Observable, Observer, Producer

Observable — prvi osnovni tip koji ćemo razmotriti. Ova klasa sadrži glavni deo implementacije RxJs. Ona je povezana s posmatranim tokom kojem se može pristupiti putem metode subscribe.

U Observable se implementira pomoćni mehanizam za kreiranje ažuriranja, takozvani Observer. Izvor vrednosti za Observer se naziva Producer. To može biti niz, iterator, web soket, 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šku, ako se sekvenca završila 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 u JavaScriptu. ( Callback, Promise, RxJs )

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

Misli na glas

I tu sam shvatio da je zanimljivije pričati nego pisati o tome. 😀

Subscription

Kada se pretplatimo na tok, kreiramo novu klasu subscription, koja nam omogućava da otkažemo pretplatu putem metode unsubscribe. Takođe možemo grupisati pretplate pomoću metode add. Pa je logično da možemo odgrupisati tokove pomoću remove. Metode add i remove prihvataju drugu pretplatu kao argument. Voleo bih naglasiti da kada se odjavimo, odjavljujemo se od svih podpretplata kao da smo i njima pozvali metodu unsubscribe. Idemo dalje.

Vrste tokova

HOTCOLD
Producer se kreira spolja observableProducer se kreira unutar observable
Podaci se prenose prilikom kreiranja observablePodaci se dostavljaju prilikom pretplate
Potrebna je dodatna logika za odjavuTok se završava samostalno
Koristi vezu jedan-na-mnogeKoristi vezu jedan-na-jedan
Sve pretplate imaju jedinstvenu vrednostPretplate su nezavisne
Podaci se mogu izgubiti ako nema pretplatePonovno izdaje sve vrednosti toka za novu pretplatu

Ako bismo povukli analogiju, predstaviću vrući tok kao film u bioskopu. U kom trenutku si došao, od tog trenutka i počinješ gledanje. Hladni tok bih uporedio sa pozivom u tehničku podršku. Svaki pozivatelj sluša snimak automatskog odgovara od početka do kraja, ali možeš prekinuti vezu putem unsubscribe.

Želio bih napomenuti da postoje takozvani topli tokovi (ovakvo se određivanje rijetko susreće, i to samo u stranim zajednicama) — to je tok koji se transformira iz hladnog u vrući. Postavlja se pitanje — gdje ga koristiti? Pružit ću primjer iz prakse.

Radim s Angularom. Aktivno koristi RxJS. Za dobivanje podataka s poslužitelja očekujem hladni tok i taj tok koristim u predlošku uz pomoć asyncPipe. Ako koristim ovu cijev nekoliko puta, vraćajući se na definiciju hladnog toka, svaka cijev će zahtijevati podatke s poslužitelja, što je blago rečeno čudno. A ako transformiram hladni tok u topli, upit će se izvršiti samo jednom.

Uopće, razumijevanje vrsta tokova prilično je složeno za početnike, ali važno je.

Operatori

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

Moguće je proširiti mogućnosti rada s tokovima putem operatora. Oni pomažu u kontroliranju događaja koji se odvijaju u Observable-u. Razmotrit ćemo nekoliko najpopularnijih, a detaljnije informacije o operatorima možete pronaći na vezama u korisnim informacijama.

Operatori — of

Počnimo s pomoćnim operatorom of. On stvara Observable na temelju jednostavne vrijednosti.

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

Operatori — filter

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

Operator filtriranja filter, kao što se može naslutiti iz naziva, filtrira signal toka. Ako operator vraća istinu, propušta dalje.

Operatori — take

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

take — Prima vrijednost broja emitiranih, nakon čega završava tok.

Operatori — debounceTime

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

debounceTime — odbacuje emitirane vrijednosti koje ulaze u određeni vremenski interval između izlaznih podataka — nakon proteka tog vremenskog intervala emitira posljednju vrijednost.

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

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emituje vrijednost svake 1000ms
  setInterval(() => {
    observer.next(i++)
  }, 1000);

  // Emituje vrijednost svake 1500ms
  setInterval(() => {
    observer.next(i++)
  }, 1500);
}).pipe(
  debounceTime(700),  // Čekamo 700ms prije obrade vrijednosti
  take(3)
);  

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

Operatori — takeWhile

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

Emitira vrijednosti sve dok takeWhile ne vrati false, nakon čega se odjavljuje od toka.

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

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emitira vrijednost svakih 1000ms
  setInterval(() => {
    observer.next(i++)
  }, 1000);
}).pipe(
  takeWhile(producer => producer < 5)
);  

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

Operatori — combineLatest

Kombinirani operator combineLatest je nalik promise.all. Spaja nekoliko tokova u jedan. Nakon što svaki tok izvrši barem jednu emitaciju, dobijamo posljednje vrijednosti iz svakog u obliku niza. Zatim, nakon bilo koje emitacije iz spojenih tokova, vratit će nove vrijednosti.

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

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

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Emitira vrijednost svakih 1000ms
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emitira vrijednost svakih 750ms
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

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

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

Operatori — zip

Zip čeka vrijednost iz svakog toka i formira niz na temelju tih vrijednosti. Ako vrijednost ne dođe iz nekog toka, grupa neće biti formirana.

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

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

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Emitira vrijednost svakih 1000ms
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emitira vrijednost svakih 750
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Emitira vrijednost svakih 500
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
});

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

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

Operatori — forkJoin

forkJoin također spaja tokove, ali emitira vrijednost samo kada su svi tokovi završeni (complete).

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

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

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

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

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

forkJoin(observer_1, observer_2, observer_3);

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

Operatori — map

Transformacijski operator map pretvara vrijednost emitacije u novu.

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

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

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

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

Operatori – share, tap

Operator tap omogućuje izvršavanje side efekata, tj. bilo koje radnje koje ne utječu na tok.

Utentni operator share može pretvoriti hladni tok u vrući.

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

Završili smo s operatorima. Prelazimo na Subject.

Misli na glas

I tad sam otišao piti čaj. Umorili su me ti primjeri 😀

Obitelj subject-a

Obitelj subject-a je izvrstan primjer vrućih tokova. Ove klase su neka vrsta hibrida koje istovremeno djeluju kao observable i observer. Budući da je subject vrući tok, potrebno je otkazati pretplatu. Što se tiče osnovnih metoda, to su:

  • next – prijenos novih podataka u tok
  • error – greška i završetak toka
  • complete – završetak toka
  • subscribe – pretplatiti se na tok
  • unsubscribe – odjaviti se s toka
  • asObservable – pretvoriti u promatrača
  • toPromise – pretvara u promisu

Ističe se 4 do 5 vrsta subject-a.

Misli na glas

Na streamu sam govorio o 4, a ispostavilo se da su još jedan dodali. Kako se kaže, živite i učite.

Jednostavni Subject new Subject()– najjednostavnija vrsta subject-a. Stvara se bez parametara. Prenosi vrijednosti koje dolaze samo nakon pretplate.

BehaviorSubject new BehaviorSubject( defaultData ) – po mom mišljenju najčešće korištena vrsta subject-a. Prima zadanu vrijednost. Uvijek čuva podatke posljednjeg emitiranja koje šalje prilikom pretplate. Ova klasa takođe ima koristan metod value, koji vraća trenutno stanje toka.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) – može opcionalno primiti kao prvi argument veličinu spremnika vrijednosti koje će čuvati, a kao drugi vrijeme tijekom kojeg su potrebne promjene.

AsyncSubject new AsyncSubject() – prilikom pretplate ništa se ne događa i vrijednost će biti vraćena samo prilikom complete. Vraća se samo posljednja vrijednost toka.

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig | Observable, destination?: Observer) – o njemu dokumentacija šuti i ja ga prvi put vidim. Tko zna što radi, javite se, dopunit ćemo.

Uf. Pa, obradili smo sve što sam htio danas reći. Nadam se da su informacije bile korisne. Samostalno se možete informirati o popisu literature u kartici korisne informacije.

Korisne informacije

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster