Tere kõigile. Siin on Omelnitski Sergei. Hiljuti viisin läbi voogedastuse reaktiivprogrammeerimise kohta, kus rääkisin asünkroonsusest JavaScriptis. Täna tahaksin selle materjali kokku võtta.

Kuid enne, kui alustame põhiteemat, peame tegema sissejuhatuse. Niisiis, alustame määratlemisest: mis on virn ja järjekord?
Virn on kogum, mille elemendid saavad põhinedes "viimane sisse, first out" LIFO
Järjekord on kogum, mille elemendid saavad põhinedes "esimene sisse, esimene välja" FIFO
Selge, jätkame.

JavaScript on ühetine programmeerimiskeel. See tähendab, et seal on ainult üks teostusniit ja üks virn, kuhu funktsioonid pandakse täitmiseks järjekorda. Seega saab JavaScript korraga teostada ainult ühte operatsiooni, teised operatsioonid peavad oma järjekorda ootama virnas, kuni neid kutsutakse.
Kutsumisvirn on andmestruktuur, mis lihtsustatult öeldes salvestab teavet selle kohta, kus me programmis asume. Kui me liigume funktsiooni, paneme selle kirje virna ülempoolsesse ossa. Kui me funktsioonist tagasi tuleme, tõmbame virnast kõige ülemise elemendi ja jõuame sinna, kust seda funktsiooni kutsuti. See on kõik, mida virn suudab. Nüüd on väga huvitav küsimus. Kuidas siis töötab asünkroonsus JavaScriptis?

Tegelikult on peale virna brauserites olemas ka eriline järjekord, kus töötab nn WebAPI. Funktsioonid sellest järjekorrast täidetakse järjestikku ainult pärast seda, kui virn on täielikult tühjaks tehtud. Alles pärast seda paigutatakse need järjekorrast virna täitmiseks. Kui virnas on vähemalt üks element, siis nad ei saa virna siseneda. Just seetõttu on funktsioonide kutsumine ajavahemiku järgi tihti ajaliselt ebatäpne, kuna funktsioon ei saa pääseda järjekorrast virna, kuni see on täis.
Vaatame järgmine näide ja tegeleme selle samm-sammult "teostamisega". Samuti uurime, mis süsteemis sellega toimub.
console.log('Hi);
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
1) Seni ei toimu midagi. Brauseri konsool on puhas, kutsumisvirn on tühi.

2) Siis lisatakse käsk console.log(‘Hi’) kutsumisvirna.

3) Ja see täidetakse

4) Seejärel eemaldatakse console.log(‘Hi’) kutsumissteigist.

5) Nüüd liikume käsu setTimeout(function cb1() {… }) juurde. See lisatakse kutsumissteigi.

6) Käsk setTimeout(function cb1() {… }) täidetakse. Brauser loob taimeri, mis on osa Web API-st. See hakkab ajaloos tagasi lugema.

7) Käsk setTimeout(function cb1() {… }) lõpetab töö ja eemaldatakse kutsumissteigist.

8) Käsk console.log(‘Bye’) lisatakse kutsumissteigi.

9) Käsk console.log(‘Bye’) täidetakse.

10) Käsk console.log(‘Bye’) eemaldatakse kutsumissteigist.

11) Pärast vähemalt 5000 ms möödumist, taimer lõpetab töö ja paneb kolmanda argumendi cb1 tagumise järjekorda.

12) Ürituste tsükkel võtab funktsiooni cb1 tagumisest järjekorrast ja paneb selle kutsumissteigi.

13) Funktsioon cb1 täidetakse ja lisab console.log(‘cb1’) kutsumissteigi.

14) Käsk console.log(‘cb1’) täidetakse.

15) Käsk console.log(‘cb1’) eemaldatakse kutsumissteigist.

16) Funktsioon cb1 eemaldatakse kutsumissteigist.
Vaadakem näidet dünaamikas:

Noh, oleme uurinud, kuidas JavaScriptis asünkroonsus töötab. Nüüd räägime lühidalt asünkroonse koodi arengust.
Asünkroonse koodi areng.
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);
})
})
})
})
})
});Asünkroonne programmeerimine, nagu me seda JavaScripts tunneme, saab teostada ainult funktsioonidega. Need võivad olla edastatud nagu iga teine muutuja teistele funktsioonidele. Nii sündisid tagasikutsed. Ja see on lahe, lõbus ja energiline, kuni see muutub kurvaks, igavaks ja masendavaks. Miks? Lihtsalt:
- Koodi keerukuse suurenemisega muutub projekt kiiresti arusaamatuks mitmekordselt sisestatud plokkideks — "callback hell".
- Vea käsitlemine võib kergesti ununeda.
- Väljendeid ei saa tagastada return-iga.
Promises'i tulekuga paranes olukord veidi.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 2000);
}).then((result) => {
alert(result);
return result + 2;
}).then((result) => {
throw new Error('FAIL HERE');
alert(result);
return result + 2;
}).then((result) => {
alert(result);
return result + 2;
}).catch((e) => {
console.log('error: ', e);
});- Tekkis lubade ahel, mis parandas koodi loetavust.
- Ilmus eraldi viga püüdmise meetod.
- Ilmnes paralleelse täitmise võimalus Promise.all abil
- Sisemise asünkroonsuse saame lahendada async/await abil
Kuid lubadusel on oma piirangud. Näiteks lubadust ei saa tühistada ilma eriliste pingutusteta, ja mis veelgi olulisem - see töötab ühe väärtusega.
Noh, oleme sujuvalt jõudnud reaktiivse programmeerimise juurde. Kas olete väsinud? Õnneks on võimalik minna tee keetmiseks, mõelda ja naasta lugema edasi. Aga mina jätkan.

Reaktiivne programmeerimine— programmiskeem, mis keskendub andmevoogudele ja muutuste levitamisele. Vaatame lähemalt, mis on andmevoog.
// Получаем ссылку на элемент
const input = ducument.querySelector('input');
const eventsArray = [];
// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
event => eventsArray.push(event)
);Kujutame ette, et meil on sisestusväli. Loome massiivi ja salvestame iga input'i keyup sündmuse meie massiivi. Samuti tahan märkida, et meie massiiv on järjestatud ajaliselt, st hilisemate sündmuste indeks on suurem kui varasemate indeks. Selline massiiv esindab lihtsustatud andmevoogu, kuid see pole veel voog. Et seda massiivi saaks julgesti nimetada vooguks, peab see suutma mingil viisil teavitada tellijaid, et sinna on tulnud uusi andmeid. Seega jõudsime andmevoo määratlemiseni.
Andmevoog
const { interval1 } = Rx;
const { take } = RxOperators;
interval(1000).pipe(
take(4)
)
Voog— on ajaliselt järjestatud andmemassiiv, mis võib teavitada andmete muudatustest. Kujutage nüüd ette, kui mugav on kirjutada koodi, kus ühe tegevuse jaoks tuleb kutsuda mitu sündmust erinevates koodilõikudes. Me lihtsalt tellime voogu ja see teavitab meid automaatselt, kui muudatused toimuvad. Ja seda suudab RxJs teek.

RxJS — on teek asünkroonsete ja sündmustel põhinevate programmide jaoks, kasutades jälgitavaid järjestusi. Teek pakub põhityüpi Observable, mitu abistavat tüüpi (Observer, Schedulers, Subjects) ja sündmuste töötlemise operaatorid nagu kogumite puhul (map, filter, reduce, every ja sarnased JavaScripti massiivist).
Vaatame põhikontseptsioone, mis selles teegis on.
Observable, Observer, Producer
Observable on esimene põhiklass, millega me tutvume. See klass sisaldab RxJs rakenduse põhijõudu. See on seotud jälgitava vooguga, millele saab registreeruda meetodi subscribe kaudu.
Observable'is on rakendatud abimehhanism värskenduste loomiseks, nii öeldakse Observer. Kõiki Observer'i väärtusi,tootjat nimetatakse Producer. See võib olla massiiv, iteratsioon, web soket, mingi sündmus jne. Seega võib öelda, et observable on vahendaja Producer'i ja Observer'i vahel.
Observable töötleb kolme tüüpi üritusi Observer'ile:
- next – uued andmed
- error – viga, kui jada lõppes erandi tõttu. see sündmus eeldab ka jada lõpetamist.
- complete — signaal jada lõpetamiseks. See tähendab, et uusi andmeid enam ei tule.
Vaadakem demo:

Alustame väärtuste 1, 2, 3 töötlemisest, ja pärast 1 sekundit saame 4 ning lõpetame meie voolu.
Mõtisklen
Ja siis mõistsin, et rääkimine oli huvitavam kui sellest kirjutamine. 😀
Subscription
Kui me registreerume voogule, loome uue klassi subscription, mis võimaldab meil tühistada tellimuse meetodi unsubscribekaudu. Samuti saame tellimusi rühmitada meetodi kaudu add. Ja on mõistetav, et saame vooge eraldada meetodi abil eemalda. Meetodid add ja remove võtavad sisse teise tellimuse. Tahaksin märkida, et kui me tühistame tellimuse, siis tühistame me ka kõik alamtellimused just nagu oleksime neile käskinud meetodit unsubscribe. Jätkame.
Vood tüübid
| HOT | COLD |
|---|---|
| Producer luuakse observable'ist väljaspool | Producer luuakse observable'i sees |
| Andmed edastatakse observable'i loomise hetkel | Andmed edastatakse tellimise hetkel |
| Tuleb lisaloogika tühistamiseks | Voo lõpetab iseennast |
| Kasutab ühte-korra-mitmele seondumist | Kasutab ühe-korra-ühele seondumist |
| Kõik tellimused omavad ühte väärtust | Tellimused on sõltumatud |
| Andmed võivad kaduda, kui tellimust ei ole | Taaskäideldakse kõiki voolu väärtusi uus tellimus |
Kui tuua analoogia, siis kujutaksin kuuma voolu nagu filmi kinos. Millal sa kohale jõudsid, sellest hetkest alates hakad vaatama. Külma voolu võiksin võrrelda tehnilise toe kõnega. Iga helistaja kuulab vastusautomaatide salvestust algusest lõpuni, kuid saad telefonikõne katkestada meetodiga unsubscribe.
Tahaksin märkida, et eksisteerivad ka nn soojad vood (seda määratlust kohtasin äärmiselt harva ja ainult välisriikide kogukondades) — see on voog, mis transformeerub külmast voost kuumaks. Küsitav on, kus seda kasutada)) Toon näite praktikast.
Töötan angulaariga. See kasutab aktiivselt rxjs-i. Serverist andmete saamiseks ootan külma voogu ja kasutan seda šabloonis koos asyncPipe'iga. Kui kasutan seda pipa mitu korda, siis naastes külma voolu määratlemise juurde, taotleb iga pipa andmeid serverist, mis on pehmelt öeldes kummaline. Kui aga muudan külma voogu soojaks, siis toimub päring üks kord.
Üldiselt on voogude tüüpide mõistmine algajatele piisavalt keeruline, kuid oluline.
Operaatorid
return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
.pipe(
tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
map(({ data }: TradeCompanyList) => data)
);Operaatorid pakuvad meile laiendatud võimalusi voogudega töötamiseks. Need aitavad kontrollida Observable'is toimuvaid sündmusi. Käsitleme mõnda kõige populaarsemat, kuid operaatoritega saab lähemalt tutvuda kasuliku info lingil.
Operaatorid — of
Alustame abistava operaatori of'iga. See loob Observable'i lihtsast väärtusest.

Operaatorid — filter

Filteroperaator filter, nagu nimi ütleb, filtreerib voolu signaali. Kui operaator tagastab tõe, siis lastakse see edasi.
Operaatorid — take

take — Võtab vastu emitatsioonide arvu, pärast mida lõpetab voo.
Operaatorid — debounceTime

debounceTime — viskab kõrvale emitatsioonid, mis satuvad määratud aja vahemikku väljundite vahel — pärast ajavahemiku möödumist emititakse viimane väärtus.
const { Observable } = Rx;
const { debounceTime, take } = RxOperators;
Observable.create((observer) => {
let i = 1;
observer.next(i++);
// Emitime väärtuse iga 1000 ms järel
setInterval(() => {
observer.next(i++)
}, 1000);
// Emitime väärtuse iga 1500 ms järel
setInterval(() => {
observer.next(i++)
}, 1500);
}).pipe(
debounceTime(700), // Ootame 700 ms väärtuse enne töötlemist
take(3)
); 
Operaatorid — takeWhile

Emitib väärtusi, kuni takeWhile ei tagasta false, pärast mida loobub voost.
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)
); 
Operators — combineLatest
Kombineeritud operaator combineLatest sarnaneb promise.all-iga. See ühendab mitmed vood ühte. Pärast seda, kui iga voog on teinud vähemalt ühe emit'i, saame viimasest väärtused igaühe kohta massiivina. Edasi, pärast mida tahes ühte emit'i ühendatud voogudest, annab see uusi väärtusi.

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));
Operators — zip
Zip ootab väärtust igast voost ja moodustab massiivi nende väärtuste põhjal. Kui väärtus ei tule üheltki voost, siis gruppi ei moodustata.

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 750
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
});
const observer_3 = Observable.create((observer) => {
let i = 1;
// Emit value every 500
setInterval(() => {
observer.next('c: ' + i++);
}, 500);
});
zip(observer_1, observer_2, observer_3).pipe(take(5));
Operators — forkJoin
forkJoin ühendab ka vood, kuid emit'ib väärtuse ainult siis, kui kõik vood on lõpetatud (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 750
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
}).pipe(take(5));
const observer_3 = Observable.create((observer) => {
let i = 1;
// Emit value every 500
setInterval(() => {
observer.next('c: ' + i++);
}, 500);
}).pipe(take(4));
forkJoin(observer_1, observer_2, observer_3);
Operators — map
Muutmisoperaator map muudab emit'i väärtuse uueks.

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

Operators – share, tap
Operator tap võimaldab teha kergelt kõrvalisi efekte, s.t. teisi tegevusi, mis ei mõjuta järjestust.
Abiooperator share suudab külma voolu muuta kuumaks.

Oleme operaatoritega lõpetanud. Liigume subjektide juurde.
Mõtisklen
Ja siis läksin ma teed jooma. Need näited väsitasid mind 😀
Subjektide perekond
Subjektide perekond on ilmekas näide kuumadest voogudest. Need klassid on teatud hübriid, mis toimivad samal ajal nii observable kui observerina. Kuna subjekt on kuum voog, tuleb sellelt maha registreerida. Peamised meetodid on:
- next – andmete edastamine voogu
- error – voogu lõpetamine ja viga
- complete – voogu lõpetamine
- subscribe – voogu registreerimine
- unsubscribe – voogust maha registreerimine
- asObservable – muudame vaatlejaks
- toPromise – muudab lubaduseks
Erakordselt on 4-5 tüüpi subjekte.
Mõtisklen
Streamil rääkisin 4, aga selgus, et nad on ühe veel lisanud. Nagu öeldakse, elad ja õpid.
Lihtne Subject new Subject()– kõige lihtsam tüüp subjekte. Loob ilma parameetriteta. Edastab väärtusi, mis saabuvad vaid pärast registreerimist.
BehaviorSubject new BehaviorSubject(defaultData<T>) – minu arvates kõige levinum tüüp subjekte. Võtab sisendiks vaikeväärtuse. Hoidub alati viimase edastuse andmetest, mille ta edastab registreerimise ajal. Sellel klassil on samuti kasulik meetod value, mis tagastab voolu praeguse väärtuse.
ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) – Võtab sisendiks valikuliselt esmase argumendina väärtuste puhvri suuruse, mille ta enda sisse salvestab, ja teise argumentina aja, mille jooksul on meil muudatused vajalikud.
AsyncSubject new AsyncSubject() – registreerimisel ei toimu midagi, ja väärtus naastes ainult siis, kui voog on lõpetatud. Tagastatakse ainult voolu viimane väärtus.
WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) – Dokumentatsioon selle kohta ei räägi ja näen seda esmakordselt. Kes teab, mida see teeb, kirjutage, täiendame.
Ohh. Noh, oleme nüüd läbinud kõik, mida tahtsin täna jagada. Loodan, et see teave oli kasulik. Iseseisvalt saab kirjanduse nimekirjaga tutvuda kasulike info sektsioonist.
Kasulik teave
- – link streamile
- – Kuidas JS töötab: ülevaade mootori, käitamise mehhanismide, väljakutsumise virna kohta
- — Kuidas töötab JS: sündmuste tsükkel, asünkroonsus ja viis viisi koodi parendamiseks async / await abil
- — Kuidas töötab Event Loop JavaScriptis
- — Asünkroonse JavaScripti evolutsioon
- — Mis on RxJS ja miks on sellest kasulik teada
- — Praktiline rakendamine RxJS
- — RxJS Observablesi õpetus — loomine ja tellimine observablest
- — RXJS: kuumad ja külmad observablid
- — Klassid, funktsioonid observablite loomiseks. Operiatorid.
- — RxJS operaatorid näite kaudu
- — API loetelu
- — Subjectide ja ajakava tüübid RxJS-is
Allikas: habr.com
