ProHoster > Blog > uprava > Asinkrono programiranje u JavaScriptu (Callback, Promise, RxJs)
Asinkrono programiranje u JavaScriptu (Callback, Promise, RxJs)
Bok svima. U kontaktu Omelnitsky Sergey. Ne tako davno, vodio sam stream o reaktivnom programiranju, gdje sam govorio o asinkroniji u JavaScriptu. Danas bih želio sažeti ovaj materijal.
Ali prije nego počnemo s glavnim materijalom, moramo napraviti uvod. Pa počnimo s definicijama: što su stog i red?
Stog je kolekcija čiji se elementi dohvaćaju prema LIFO principu "zadnji ušao, prvi izašao".
Red je zbirka čiji se elementi dobivaju po principu (“prvi ušao, prvi izašao” FIFO
U redu, nastavimo.
JavaScript je jednonitni programski jezik. To znači da ima samo jednu nit izvršavanja i jedan stog gdje su funkcije u redu čekanja za izvršenje. Stoga JavaScript može izvoditi samo jednu operaciju u isto vrijeme, dok će druge operacije čekati svoj red na stogu dok se ne pozovu.
Stog poziva je podatkovna struktura koja, jednostavno rečeno, bilježi podatke o mjestu u programu na kojem se nalazimo. Ako skočimo u funkciju, guramo njen unos na vrh stoga. Kada se vratimo iz funkcije, izbacimo najviši element sa stoga i završimo tamo odakle smo pozvali ovu funkciju. To je sve što hrpa može. A sad jedno vrlo zanimljivo pitanje. Kako onda asinkronija radi u JavasScriptu?
Zapravo, osim hrpe, preglednici imaju poseban red čekanja za rad s takozvanim WebAPI-jem. Funkcije iz ovog reda čekanja izvršavat će se redom tek nakon što se stog potpuno očisti. Tek nakon toga se iz reda čekanja stavljaju na stog za izvršenje. Ako postoji barem jedan element na stogu u ovom trenutku, onda oni ne mogu doći na stog. Upravo zbog toga, pozivanje funkcija prema isteku vremena često je vremenski netočno, jer funkcija ne može doći iz reda čekanja na stog dok je puna.
Pogledajmo sljedeći primjer i prođimo kroz njega korak po korak. Pogledajmo i što se događa u sustavu.
1) Za sada se ništa ne događa. Konzola preglednika je čista, skup poziva prazan.
2) Zatim se naredba console.log('Hi') dodaje u skup poziva.
3) I ispunjeno je
4) Zatim se console.log('Hi') uklanja iz poziva.
5) Sada prijeđimo na naredbu setTimeout(function cb1() {… }). Dodaje se u skup poziva.
6) Izvršava se naredba setTimeout(function cb1() {… }). Preglednik stvara mjerač vremena koji je dio Web API-ja. Izvršit će odbrojavanje.
7) Naredba setTimeout(function cb1() {… }) je završila svoj posao i uklonjena je iz poziva.
8) Naredba console.log('Bye') dodaje se u skup poziva.
9) Naredba console.log('Bye') je izvršena.
10) Naredba console.log('Bye') je uklonjena iz poziva.
11) Nakon što je prošlo najmanje 5000 ms, mjerač vremena završava i stavlja cb1 povratni poziv u red čekanja za povratni poziv.
12) Petlja događaja preuzima funkciju cb1 iz reda povratnih poziva i gura je na stog poziva.
13) Funkcija cb1 se izvršava i dodaje console.log('cb1') u stog poziva.
14) Izvršena je naredba console.log('cb1').
15) Naredba console.log('cb1') je uklonjena iz skupa poziva.
16) Funkcija cb1 je uklonjena iz poziva.
Pogledajmo primjer u dinamici:
Pa, pogledali smo kako je asinkronija implementirana u JavaScriptu. Razgovarajmo sada ukratko 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 poznajemo u JavaScriptu može se izvesti samo s funkcijama. Mogu se proslijediti drugim funkcijama kao i svaka druga varijabla. Tako su rođeni povratni pozivi. I cool je, zabavno i žarko, sve dok ne preraste u tugu, melankoliju i tugu. Zašto? Da, jednostavno je:
Kako složenost koda raste, projekt se brzo pretvara u nejasne višestruke ugniježđene blokove - "pakao povratnog poziva".
Rješavanje pogrešaka može se lako previdjeti.
Ne možete vratiti izraze s return.
Dolaskom Promisea situacija je postala malo bolja.
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 su se lanci obećanja, što je poboljšalo čitljivost koda
Postojala je posebna metoda presretanja pogrešaka
Paralelno izvođenje s Promise.all dodano
Ugniježđenu asinkroniju možemo riješiti s async/await
Ali obećanje ima svoja ograničenja. Na primjer, obećanje, bez plesa s tamburinom, ne može se poništiti, a što je najvažnije, radi s jednom vrijednošću.
Pa, ovdje se glatko približavamo reaktivnom programiranju. Umoran? Pa, dobra stvar je što možete otići skuhati galebove, razmisliti i vratiti se čitati više. I nastavit ću.
Reaktivno programiranje - programska paradigma usmjerena na tokove podataka i širenje promjena. Pogledajmo pobliže što je tok podataka.
// Получаем ссылку на элемент
const input = ducument.querySelector('input');
const eventsArray = [];
// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
event => eventsArray.push(event)
);
Zamislimo da imamo polje za unos. Stvaramo niz i za svaki unos tipke ulaznog događaja pohranit ćemo događaj u naš niz. U isto vrijeme, želio bih napomenuti da je naš niz razvrstan po vremenu, tj. indeks kasnijih događaja veći je od indeksa ranijih događaja. Takav niz je pojednostavljeni model toka podataka, ali još nije tok. Da bi se ovaj niz sa sigurnošću mogao nazvati streamom, on mora moći na neki način obavijestiti pretplatnike da su u njega stigli novi podaci. Tako dolazimo do definicije toka.
potok je niz podataka poredanih po vremenu koji može značiti da su se podaci promijenili. Zamislite sada kako postaje zgodno pisati kod u kojem trebate pokrenuti nekoliko događaja u različitim dijelovima koda za jednu radnju. Jednostavno se pretplatimo na stream i on će nam javiti kada dođe do promjena. I biblioteka RxJs to može učiniti.
RxJS je knjižnica za rad s asinkronim programima i programima temeljenim na događajima koji koriste vidljive sekvence. Knjižnica pruža glavni tip primjetan, nekoliko vrsta pomoćnika (Promatrači, planeri, subjekti) i operatori za rad s događajima kao i s zbirkama (mapirati, filtrirati, smanjiti, svaki i slični iz JavaScript Array).
Hajdemo razumjeti osnovne koncepte ove knjižnice.
Uočljiv, promatrač, proizvođač
Observable je prvi osnovni tip koji ćemo pogledati. Ova klasa sadrži glavni dio RxJs implementacije. Povezan je s vidljivim tokom, na koji se možete pretplatiti metodom pretplate.
Observable implementira pomoćni mehanizam za kreiranje ažuriranja, tzv Posmatrač. Izvor vrijednosti za promatrača se zove Producent. To može biti niz, iterator, web socket, neka vrsta događaja itd. Dakle, možemo reći da je observable dirigent između proizvođača i promatrača.
Observable obrađuje tri vrste Observer događaja:
sljedeći - novi podaci
pogreška - pogreška ako je niz prekinut zbog iznimke. ovaj događaj također implicira kraj niza.
kompletan - signal o kraju niza. To znači da više neće biti novih podataka
Pogledajmo demo:
Na početku ćemo obraditi vrijednosti 1, 2, 3, a nakon 1 sek. dobijemo 4 i završimo našu nit.
Razmišljajući naglas
I tada sam shvatio da je zanimljivije pričati nego pisati o tome. 😀
Pretplata
Kada se pretplatimo na stream, stvaramo novu klasu pretplata, što nam daje mogućnost da odjavimo pretplatu metodom unsubscribe. Također možemo grupirati pretplate korištenjem metode dodati. Pa, logično je da možemo razgrupirati niti pomoću ukloniti. Metode dodavanja i uklanjanja prihvaćaju različite pretplate kao unos. Želio bih napomenuti da kada odjavljujemo pretplatu, odjavljujemo sve podređene pretplate kao da su također pozvali metodu odjave. Samo naprijed.
Vrste potoka
vRUĆE
HLADNO
Proizvođač je stvoren izvan vidljivog
Proizvođač je stvoren unutar vidljivog
Podaci se prosljeđuju u trenutku kada je observable kreiran
Podaci se daju u trenutku pretplate.
Treba više logike za odjavu
Nit se prekida sama od sebe
Koristi odnos jedan prema više
Koristi odnos jedan-na-jedan
Sve pretplate imaju istu vrijednost
Pretplate su neovisne
Podaci se mogu izgubiti ako nema pretplate
Ponovno izdaje sve vrijednosti streama za novu pretplatu
Da dam analogiju, zamislio bih vrući tok poput filma u kinu. U kojem ste trenutku došli, od tog trenutka ste počeli gledati. Hladni potok usporedio bih sa zovom u njima. podrška. Svaki pozivatelj sluša snimku telefonske sekretarice od početka do kraja, ali možete prekinuti vezu uz odjavu.
Napominjem da postoje i takozvani topli tokovi (takvu definiciju sam susreo izuzetno rijetko i to samo u stranim zajednicama) - to je tok koji se pretvara iz hladnog toka u vrući. Postavlja se pitanje - gdje koristiti)) Dat ću primjer iz prakse.
Radim s Angularom. Aktivno koristi rxjs. Za prijenos podataka na poslužitelj očekujem hladni tok i koristim ovaj tok u predlošku koristeći asyncPipe. Ako ovu cijev koristim nekoliko puta, tada će, vraćajući se na definiciju hladnog toka, svaka cijev tražiti podatke od poslužitelja, što je u najmanju ruku čudno. A ako pretvorim hladni tok u topli, tada će se zahtjev dogoditi jednom.
Općenito, razumijevanje vrste tokova prilično je teško za početnike, ali važno.
Operateri
return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
.pipe(
tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
map(({ data }: TradeCompanyList) => data)
);
Operateri nam daju mogućnost rada sa streamovima. Oni pomažu kontrolirati događaje koji teku u Observable. Razmotrit ćemo nekoliko najpopularnijih, a više informacija o operaterima možete pronaći na poveznicama u korisnim informacijama.
Operateri-od
Počnimo s pomoćnim operatorom of. Stvara Observable na temelju jednostavne vrijednosti.
Operatori-filtar
Operator filtera, kao što ime sugerira, filtrira signal toka. Ako operator vrati true, tada se preskače dalje.
Operateri - uzeti
take - Uzima vrijednost broja emitiranja, nakon čega se stream završava.
Operatori-debounceTime
debounceTime - odbacuje emitirane vrijednosti koje spadaju unutar navedenog vremenskog intervala između izlaznih podataka - nakon što vremenski interval prođe, emitira posljednju vrijednost.
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)
);
Operatori-takeWhile
Emitira vrijednosti dok takeWhile ne vrati false, a zatim se odjavljuje s niti.
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 )
);
Operatori-kombajnNajnovije
Kombinirani operator combineLatest donekle je sličan promise.all. Kombinira više tokova u jedan. Nakon što svaka nit napravi barem jedno emitiranje, dobivamo najnovije vrijednosti od svake kao niz. Nadalje, nakon bilo kakvog emitiranja iz kombiniranih tokova, to će dati nove vrijednosti.
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));
Operatori-zip
Zip - čeka vrijednost iz svakog toka i formira niz na temelju tih vrijednosti. Ako vrijednost ne dolazi ni iz jedne niti, tada se grupa neće formirati.
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));
Operatori - forkJoin
forkJoin također spaja niti, ali emitira vrijednost samo kada su sve niti dovršene.
const { forkJoin, Observable } = Rx;
const { take } = RxOperators;
const observer_1 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next('a: ' + i++);
}, 1000);
}).pipe(take(3));
const observer_2 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 750
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
}).pipe(take(5));
const observer_3 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 500
setInterval(() => {
observer.next('c: ' + i++);
}, 500);
}).pipe(take(4));
forkJoin(observer_1, observer_2, observer_3);
Operatori-mapa
Operator transformacije karte transformira emitiranu vrijednost u novu.
const { Observable } = Rx;
const { take, map } = RxOperators;
Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next(i++);
}, 1000);
}).pipe(
map(x => x * 10),
take(3)
);
Operatori - dijeljenje, dodir
Operator slavine vam omogućuje da radite nuspojave, odnosno sve radnje koje ne utječu na slijed.
Operater dijeljenja komunalnih usluga može pretvoriti hladni tok u vrući tok.
Operatori su gotovi. Prijeđimo na Predmet.
Razmišljajući naglas
A onda sam otišao popiti čaj. Umoran sam od ovih primjera 😀
Obitelj predmeta
Predmetna obitelj najbolji je primjer vrućih niti. Ove klase su svojevrsni hibridi koji djeluju kao vidljivi i promatrači u isto vrijeme. Budući da je tema vrući stream, morate se odjaviti s nje. Ako govorimo o glavnim metodama, onda su to:
sljedeći - prosljeđivanje novih podataka u tok
error - greška i prekid niti
dovršeno - kraj konca
subscribe - pretplatite se na stream
unsubscribe - odjava pretplate na stream
asObservable - transformirati se u promatrača
toPromise - pretvara se u obećanje
Dodijelite 4 5 vrsta predmeta.
Razmišljajući naglas
Rekao sam 4 na streamu, ali ispalo je da su dodali još jednog. Kako se kaže, živi i uči.
Jednostavan predmet new Subject()- najjednostavnija vrsta predmeta. Stvoreno bez parametara. Prolazi vrijednosti koje su došle tek nakon pretplate.
BehaviorSubject new BehaviorSubject( defaultData<T> ) - po mom mišljenju najčešći tip subjekta. Unos ima zadanu vrijednost. Uvijek sprema podatke zadnjeg broja, koji se prenose prilikom pretplate. Ova klasa također ima korisnu metodu vrijednosti koja vraća trenutnu vrijednost toka.
ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) - Po želji, kao prvi argument može uzeti veličinu međuspremnika vrijednosti koje će pohraniti u sebe, a drugi vrijeme tijekom kojeg su nam potrebne promjene.
asinkroni subjekt new AsyncSubject() - ništa se ne događa prilikom pretplate, a vrijednost će biti vraćena tek kada se završi. Vratit će se samo zadnja vrijednost toka.
WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - Dokumentacija o tome šuti i ja to prvi put vidim. Tko zna što radi, napišite, mi ćemo dodati.
Fuj. Pa, razmotrili smo sve što sam danas htio reći. Nadam se da su ove informacije bile korisne. Popis literature možete sami pročitati u kartici Korisne informacije.