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.

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.

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?

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');
1) Zatiaľ sa nič nedeje. Konzola prehliadača je prázdna, zásobník volaní je prázdny.

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

3) A on sa vykonáva

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

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

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.

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

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

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

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

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

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

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

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

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

16) Funkcia cb1 sa odstráni zo zásobníka volaní.
Pozrime sa na príklad v dynamike:

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.

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

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:

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
| HOT | COLD |
|---|---|
| Producer sa vytvára zvonku observable | Producer 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ásenie | Prúd sa končí sám |
| Používa spojenie jeden-k-mnohým | Používa spojenie typu jeden-k-jednému |
| Všetky prihlásenia majú rovnakú hodnotu | Prihlásenia sú nezávislé |
| Údaje môžu byť stratené, ak nie je prihlásenie | Prevodí 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.

Operátory — filter

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

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

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)
); 
Operátory — takeWhile

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

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

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));
Operátory — forkJoin
forkJoin tiež spája proudy, ale emitentuje hodnotu iba vtedy, keď sú všetky prúdy dokončené (complete).

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);
Operátory — map
Transformačný operátor map mení hodnotu emitovania na novú.

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

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.

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
- — odkaz na stream
- — Ako funguje JS: prehľad enginu, mechanizmov runtime, zásobníka volaní
- — Ako funguje JS: cyklus udalostí, asynchronicita a päť spôsobov, ako zlepšiť kód pomocou async / await
- — Ako funguje Event Loop v JavaScripte
- — Evolúcia asynchrónneho JavaScriptu
- — Čo je RxJS a prečo je dobré o ňom vedieť
- — Praktické využitie RxJS
- — RxJS Observables Tutorial — Creating & Subscribing to Observables
- — RXJS: Horúce a studené observables
- — Triedy a funkcie na vytvorenie Observable. Operátory.
- — RxJS Operátory podľa príkladov
- — API Zoznam
- — Rôzne typy Subject a Rozvrhy v RxJS
Zdroj: habr.com
