Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Ahoj všetci. Tu je Omelnytskyi Serhii. Nedávno som mal live stream o reaktívnom programovaní, kde som rozprával o asynchrónnosti v JavaScripte. Dnes by som chcel zhrnúť tento materiál.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Ale predtým, ako začneme s hlavným materiálom, musíme urobiť úvod. Poďme si teda najprv definovať, čo je zásobník a fronta?
Zásobník je kolekcia, z ktorej sú prvky vyberané podľa princípu „posledný dodal, prvý vytiahnutý“ LIFO.
Fronta je kolekcia, z ktorej sú prvky vyberané podľa princípu „prvý dodal, prvý vytiahnutý“ FIFO.

Dobre, pokračujeme.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

JavaScript je jednoprevádzkový programovací jazyk. To znamená, že má iba jeden tok vykonávania a jeden zásobník, do ktorého sú funkcie umiestňované na vykonanie. Preto môže JavaScript v jeden moment vykonať iba jednu operáciu, ostatné operácie budú čakať na svoju rad v zásobníku, kým nebudú vyvolané.

Zásobník volaní je štruktúra údajov, ktorá, zjednodušene povedané, zaznamenáva informácie o tom, kde sa nachádzame v programe. Keď prechádzame do funkcie, pridáme záznam o nej na vrchol zásobníka. Keď sa vraciame z funkcie, vytiahneme z najvrchnejšieho prvku zásobníka a ocitneme sa tam, odkiaľ sme túto funkciu volali. To je všetko, čo zásobník dokáže. A teraz veľmi zaujímavá otázka. Ako teda funguje asynchrónnosť v JavaScript-e?

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

V skutočnosti okrem zásobníka existuje v prehliadačoch špeciálna fronta na prácu s takzvaným WebAPI. Funkcie z tejto fronty sa vykonajú postupne až po úplnom vyčistení zásobníka. Až potom sú presunuté z fronty do zásobníka na vykonanie. Ak je v zásobníku aspoň jeden prvok, funkcie nemôžu vstúpiť do zásobníka. Presne z tohto dôvodu je vyvolanie funkcií po timeout-e často časovo nepresné, pretože funkcia nemôže vstúpiť z fronty do zásobníka, kým je ten plný.

Pozrime sa na nasledujúci príklad a zaoberať sa jeho krokovým „vykonaním“. Taktiež preskúmame, čo sa dejú v systéme.

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

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

1) Zatiaľ sa nič nedeje. Konzola prehliadača je prázdna, zásobník volaní je prázdny.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

2) Potom príkaz console.log(‘Hi’) je pridaný do zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

3) A on sa vykonáva

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

4) Následne console.log('Hi') sa odstráni z zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

5) Teraz prechádzame k príkazu setTimeout(function cb1() {… }). Ten sa pridáva do zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

6) Príkaz setTimeout(function cb1() {… }) sa vykonáva. Prehliadač vytvára časovač, ktorý je súčasťou Web API. Ten začne odpočítavať čas.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

7) Príkaz setTimeout(function cb1() {… }) ukončil svoju prácu a odstráni sa z zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

8) Príkaz console.log('Bye') sa pridáva do zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

9) Príkaz console.log('Bye') sa vykonáva.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

10) Príkaz console.log('Bye') sa odstráni zo zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

11) Keď uplynú minimálne 5000 ms, časovač ukončí svoju prácu a umiestni kolbek cb1 do fronty kolbekov.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

12) Cykel udalostí zoberie funkciu cb1 z fronty kolbekov a umiestni ju do zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

13) Funkcia cb1 sa vykonáva a pridáva console.log('cb1') do zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

14) Príkaz console.log('cb1') sa vykonáva.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

15) Príkaz console.log('cb1') sa odstráni zo zásobníka volaní.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

16) Funkcia cb1 sa odstráni zo zásobníka volaní.

Pozrime sa na príklad v dynamike:

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Tak sme si prešli, ako je v JavaScripte implementovaná asynchrónnosť. Teraz si stručne poďme popísť evolúciu asynchrónneho kódu.

Evolúcia asynchrónneho kódu.

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

Asynchrónne programovanie, aké poznáme v JavaScripte, môže byť realizované iba pomocou funkcií. Môžu byť prenášané ako akákoľvek iná premenná iným funkciám. Tak sa zrodili kolbky. A je to fajn, zábavné a vzrušujúce, kým sa to nepretransformuje na smútenie a zúfalstvo. Prečo? Je to jednoduché:

  • S rastom zložitosti kódu sa projekt rýchlo mení na ťažko zrozumiteľné viacnásobne vnorené bloky — „callback hell“.
  • Spracovanie chýb je ľahké prehliadnuť.
  • Nemôžete vracať výrazy pomocou return.

S príchodom Promise sa situácia o niečo zlepšila.

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

  • Objavili sa reťazce promísov, čo zlepšilo čitateľnosť kódu.
  • Objavila sa samostatná metóda na zachytávanie chýb.
  • Objavila sa možnosť paralelného vykonávania pomocou Promise.all
  • Vloženú asynchrónnosť môžeme vyriešiť pomocou async/await

Ale s promismi sú spojené isté obmedzenia. Napríklad, promisi bez „tanec s bubnom“ nemožno zrušiť, a čo je najdôležitejšie — pracuje s jednou hodnotou.

Tak a sme pomaly prešli k reaktívnemu programovaniu. Už vás to unavuje? Našťastie si môžete ísť uvariť čaj, premyslieť a vrátiť sa čítať ďalej. Ja pokračujem.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Reaktívne programovanie— paradigma programovania zameraná na toky dát a šírenie zmien. Poďme sa podrobnejšie pozrieť na to, čo je tok dát.

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

const eventsArray = [];

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

Predstavme si, že máme vstupné pole. Vytvoríme pole a na každé udalosti keyup vstupu budeme ukladať udalosť do nášho poľa. Pri tom by sme chceli poznamenať, že naše pole je usporiadané podľa času — t.j. index neskorších udalostí je väčší než index skôr udalostí. Takéto pole predstavuje zjednodušený model toku dát, ale to ešte nie je tok. Aby sme mohli toto pole smelo nazvať tokom, musí vedieť nejakým spôsobom informovať predplatiteľov, že doň prišli nové dáta. Takto sme sa dostali k definícii toku.

Tok dát

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

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

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Tok— je pole dát usporiadaných podľa času, ktoré môže informovať o tom, že sa dáta zmenili. A teraz si predstavte, aké pohodlné je písať kód, v ktorom na jedno akciu potrebujete vyvolať niekoľko udalostí na rôznych miestach kódu. Jednoducho sa prihlásime na tok a on nám sám oznámi, kedy dôjde k zmenám. A toto dokáže knižnica RxJs.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

RxJS — je knižnica na prácu s asynchrónnymi a na udalostiach založenými programami s použitím pozorovateľných sekvencií. Knižnica poskytuje základný typ Observable, niekoľko pomocných typov (Observer, Schedulers, Subjects) a operátory na prácu s udalosťami ako s kolekciami (map, filter, reduce, every a podobné z JavaScript Array).

Poďme sa pozrieť na základné pojmy tejto knižnice.

Observable, Observer, Producer

Observable — prvý základný typ, ktorý preskúmame. Tento trieda obsahuje hlavnú časť implementácie RxJs. Je spojený s pozorovateľným prúdom, na ktorý sa dá prihlásiť pomocou metódy subscribe.

V Observable sa realizuje pomocný mechanizmus na vytváranie aktualizácií, takzvaný Observer. Zdrojom hodnôt pre Observer sa nazýva Producer. Môže to byť pole, iterátor, web socket, nejaká udalosť atď. Takže sa dá povedať, že observable je vodič medzi Producer a Observer.

Observable spracováva tri druhy udalostí Observer:

  • next – nové údaje
  • error – chyba, ak sekvencia skončila kvôli výnimke. Táto udalosť tiež znamená ukončenie sekvencie.
  • complete — signál o ukončení sekvencie. To znamená, že nebudú žiadne nové údaje.

Pozrime sa na demo:

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Na začiatku spracujeme hodnoty 1, 2, 3, a po 1 sekunde dostaneme 4 a ukončíme náš prúd.

Myšlienky nahlas

A tu som si uvedomil, že rozprávanie bolo zaujímavejšie ako písanie o tom. 😀

Subscription

Keď sa prihlásime do prúdu, vytvoríme novú triedu subscription, ktorá nám umožňuje zrušiť prihlásenie pomocou metódy unsubscribe. Takisto môžeme skupinovať prihlásenia pomocou metódy add. A logicky môžeme rozgrupovať prúdy pomocou remove. Metódy add a remove prijímajú inú prihlášku. Chcel by som poznamenať, že keď robíme odhlásenie, odhlasujeme sa od všetkých podriadených prihlásení, akoby sme pre nich vyvolávali metódu unsubscribe. Ideme ďalej.

Druhy prúdov

HOTCOLD
Producer sa vytvára zvonku observableProducer sa vytvára vo vnútri observable
Údaje sa prenášajú v čase vytvárania observableÚdaje sa oznámia v okamihu prihlásenia
Potrebná je ďalšia logika na odhláseniePrúd sa končí sám
Používa spojenie jeden-k-mnohýmPoužíva spojenie typu jeden-k-jednému
Všetky prihlásenia majú rovnakú hodnotuPrihlásenia sú nezávislé
Údaje môžu byť stratené, ak nie je prihláseniePrevodí všetky hodnoty prúdu pre novú prihlášku

Ak by som chcel uviesť analógiu, predstavil by som si horúci prúd ako film v kine. V ktoromkoľvek čase prídeš, od toho momentu začneš sledovanie. Studený prúd by som prirovnal k hovoru na technickú podporu. Každý, kto zavolá, počúva nahrávku automatického záznamníka od začiatku do konca, ale môžeš zložiť telefón pomocou unsubscribe.

Rád by som poznamenal, že existujú aj takzvané teplé toky (takéto označenie som stretol veľmi zriedkavo a len v zahraničných komunitách) — ide o tok, ktorý sa transformuje z chladného toku na horúci. Vzniká otázka — kde ho použiť)) Uvediem príklad z praxe.

Pracujem s Angulárom. Aktívne využíva rxjs. Na získanie údajov zo servera očakávam chladný tok a tento tok používam v šablóne pomocou asyncPipe. Ak tento pipe použijem niekoľkokrát, vrátiac sa k definícii chladného toku, každý pipe bude žiadať dáta zo servera, čo je jemne povedané zvláštne. A ak premením chladný tok na teplý, požiadavka prebehne len raz.

Vlastne pochopenie druhov tokov je dosť zložité pre začiatočníkov, ale dôležité.

Operátory

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 práce s tokmi nám poskytujú operátory. Pomáhajú kontrolovať udalosti, ktoré prebiehajú v Observable. Preskúmame niekoľko najpopulárnejších, podrobnejšie s operátormi sa môžete oboznámiť prostredníctvom odkazov v užitočných informáciách.

Operátory — of

Začneme so pomocným operátorom of. Vytvára Observable na základe jednoduchého hodnoty.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátory — filter

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátor filtrovania filter, ako možno pochopiť z názvu, filtruje signál toku. Ak operátor vracia pravdu, posúva ďalej.

Operátory — take

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

take — Prijíma hodnotu počtu emitovaných hodnôt, po ktorých ukončuje tok.

Operátory — debounceTime

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

debounceTime — zahodí emitované hodnoty, ktoré sa dostanú do určitého časového intervalu medzi výstupmi — po uplynutí časového intervalu emitne poslednú hodnotu.

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

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emitujeme hodnotu raz za 1000ms
  setInterval(() => {
    observer.next(i++)
  }, 1000);

  // Emitujeme hodnotu raz za 1500ms
  setInterval(() => {
    observer.next(i++)
  }, 1500);
}).pipe(
  debounceTime(700),  // Čakáme 700ms na hodnoty skôr, ako ich spracujeme
  take(3)
);  

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátory — takeWhile

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Emitne hodnoty, pokiaľ takeWhile nevráti false, po čom sa odhlási z toku.

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

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

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátory — combineLatest

Kombinovaný operátor combineLatest je niečo podobné ako promise.all. Spojuje niekoľko proudov do jedného. Akonáhle každý proud vykoná aspoň jedno emitovanie, získame posledné hodnoty z každého vo forme poľa. Ďalej, po každom emitovaní z kombinovaných proudov bude poskytovať nové hodnoty.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

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

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

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

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

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátory — zip

Zip čaká na hodnotu z každého proudu a vytvorí pole na základe týchto hodnôt. Ak hodnota nepríde z niektorého proudu, skupina nebude vytvorená.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

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

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

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

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

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

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátory — forkJoin

forkJoin tiež spája proudy, ale emitentuje hodnotu iba vtedy, keď sú všetky prúdy dokončené (complete).

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

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

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

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

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

forkJoin(observer_1, observer_2, observer_3);

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátory — map

Transformačný operátor map mení hodnotu emitovania na novú.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

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

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

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

Operátory — share, tap

Operátor tap umožňuje vykonávať vedľajšie efekty, teda akékoľvek akcie, ktoré neovplyvňujú sekvenciu.

Užitočný operátor share dokáže premeniť studený tok na horúci.

Asynchrónne programovanie v JavaScripte. ( Callback, Promise, RxJs )

S operátormi sme skončili. Prejdime k Subjectu.

Myšlienky nahlas

A tu som si šiel uvariť čaj. Tieto príklady ma unavili 😀

Rodina subject-ov

Rodina subject-ov je jasným príkladom horúcich tokov. Tieto triedy sú akýmsi hybridom, ktorý vystupuje súčasne ako observable a observer. Keďže subject je horúci tok, je potrebné sa od neho odhlásiť. Z hľadiska hlavných metód sú to:

  • next – poslanie nových dát do toku
  • error – chyba a ukončenie toku
  • complete – ukončenie toku
  • subscribe – prihlásiť sa na tok
  • unsubscribe – odhlásiť sa z toku
  • asObservable – pretransformovať na pozorovateľa
  • toPromise – pretransformuje na promiss

Vyčleňujú sa 4-5 typov subject-ov.

Myšlienky nahlas

Na streame som hovoril 4, ale zistil som, že pridali ešte jeden. Ako sa hovorí, ži a uč sa.

Jednoduchý Subject new Subject()– najjednoduchší typ subject-ov. Vytvára sa bez parametrov. Prenáša hodnoty, ktoré prichádzajú až po prihlásení.

BehaviorSubject new BehaviorSubject( defaultData<T> ) – podľa môjho názoru najrozšírenejší typ subject-ov. Berie na vstup predvolenú hodnotu. Vždy si udržuje dáta z posledného emitovania, ktoré prenáša pri prihlásení. Táto trieda má tiež užitočnú metódu value, ktorá vracia aktuálnu hodnotu toku.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) — Voliteľne môže prijať ako prvý argument veľkosť vyrovnávacej pamäte hodnôt, ktoré si bude uchovávať, a ako druhý čas, počas ktorého potrebujeme zmeny.

AsyncSubject new AsyncSubject() — pri prihlásení sa nič nedeje a hodnota bude vrátená iba pri complete. Bude vrátené iba posledné hodnotenie toku.

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) — O ňom dokumentácia mlčí a ja ho vidím po prvýkrát. Kto vie, čo robí, napíšte, doplníme.

Uf. No a to je všetko, čo som chcel dnes povedať. Dúfam, že tieto informácie boli užitočné. Samostatne sa môžete oboznámiť so zoznamom literatúry na karte užitočné informácie.

Užitočné informácie

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster