Asinkrono programiranje u JavaScript-u (povratni poziv, obećanje, RxJs)
Zdravo svima. U kontaktu Omelnitsky Sergey. Ne tako davno, vodio sam stream o reaktivnom programiranju, gdje sam govorio o asinhroniji u JavaScriptu. Danas bih želio rezimirati ovaj materijal.
Ali prije nego što počnemo s glavnim materijalom, moramo napraviti uvod. Dakle, počnimo s definicijama: šta su stog i red čekanja?
Stack je kolekcija čiji se elementi preuzimaju po principu LIFO „posljednji ušao, prvi izašao“.
Red čekanja je kolekcija čiji se elementi dobijaju po principu (“prvi ušao, prvi izašao” FIFO
Ok, nastavimo.
JavaScript je jednonitni programski jezik. To znači da ima samo jednu nit izvršenja i jedan stek gdje su funkcije stavljene u red za izvršenje. Stoga JavaScript može izvoditi samo jednu operaciju istovremeno, dok će druge operacije čekati svoj red na steku dok se ne pozovu.
Stack poziva je struktura podataka koja, jednostavno rečeno, bilježi informacije o mjestu u programu na kojem se nalazimo. Ako skočimo u funkciju, guramo njen unos na vrh steka. Kada se vratimo iz funkcije, izbacujemo najviši element iz steka i završavamo tamo odakle smo pozvali ovu funkciju. To je sve što stog može da uradi. A sada jedno veoma interesantno pitanje. Kako onda asinhroni funkcioniraju u JavasScript-u?
Zapravo, pored steka, pretraživači imaju poseban red za rad sa takozvanim WebAPI-jem. Funkcije iz ovog reda će se izvršavati po redoslijedu tek nakon što se stog potpuno očisti. Tek nakon toga se stavljaju iz reda u stog za izvršenje. Ako postoji barem jedan element na steku u ovom trenutku, onda oni ne mogu ući na stek. Upravo zbog toga, pozivanje funkcija po isteku je često netočno u vremenu, jer funkcija ne može doći iz reda u stog dok je puna.
Pogledajmo sljedeći primjer i idemo kroz njega korak po korak. Da vidimo i šta se dešava u sistemu.
1) Za sada se ništa ne dešava. Konzola pretraživača je čista, stek poziva je prazan.
2) Zatim se naredba console.log('Hi') dodaje u stog poziva.
3) I ispunjeno je
4) Zatim se console.log('Bok') uklanja iz steka poziva.
5) Sada pređimo na naredbu setTimeout(function cb1() {… }). Dodaje se u stek poziva.
6) Naredba setTimeout(function cb1() {… }) se izvršava. Pregledač kreira tajmer koji je dio Web API-ja. Izvršit će odbrojavanje.
7) Komanda setTimeout(function cb1() {… }) je završila svoj rad i uklonjena je iz steka poziva.
8) Naredba console.log('Bye') je dodana u stog poziva.
9) Izvršava se naredba console.log('Bye').
10) Naredba console.log('Bye') je uklonjena iz steka poziva.
11) Nakon što protekne najmanje 5000ms, tajmer se završava i stavlja cb1 povratni poziv u red za povratni poziv.
12) Petlja događaja preuzima funkciju cb1 iz reda povratnog poziva i gura je u stek poziva.
13) Funkcija cb1 se izvršava i dodaje console.log('cb1') u stog poziva.
14) Izvršava se naredba console.log('cb1').
15) Naredba console.log('cb1') je uklonjena iz steka poziva.
16) Funkcija cb1 je uklonjena iz steka poziva.
Pogledajmo primjer u dinamici:
Pa, pogledali smo kako je asinhronija implementirana u JavaScript-u. Hajdemo sada ukratko o evoluciji asinhronog koda.
Evolucija asinhronog 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 obaviti samo sa funkcijama. Mogu se proslijediti kao i svaka druga varijabla drugim funkcijama. Tako su rođeni povratni pozivi. I to je cool, zabavno i vatreno, sve dok se ne pretvori u tugu, melanholiju i tugu. Zašto? Da, jednostavno je:
Kako složenost koda raste, projekt se brzo pretvara u nejasne višestruko ugniježđene blokove - „pakao povratnog poziva“.
Rukovanje greškama se može lako previdjeti.
Ne možete vratiti izraze s return.
Dolaskom Promise-a 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, koji su poboljšali čitljivost koda
Postojala je posebna metoda presretanja grešaka
Paralelno izvršavanje sa dodanim Promise.all
Ugniježđenu asinhroniju možemo riješiti sa async/await
Ali obećanje ima svoja ograničenja. Na primjer, obećanje, bez plesa s tamburom, ne može se poništiti, a što je najvažnije, funkcionira s jednom vrijednošću.
Pa, ovdje se glatko približavamo reaktivnom programiranju. Umoran? Pa, dobra stvar je što možete ići skuhati galebove, razmišljati i vratiti se da čitate više. I nastaviću.
Reaktivno programiranje - paradigma programiranja fokusirana na tokove podataka i propagaciju promjena. Pogledajmo pobliže šta je 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, a za svaki unos ulaznog događaja pohranit ćemo događaj u naš niz. Istovremeno, želio bih napomenuti da je naš niz sortiran po vremenu, tj. indeks kasnijih događaja je veći od indeksa ranijih. Takav niz je pojednostavljeni model toka podataka, ali još uvijek nije tok. Da bi se ovaj niz sigurno mogao nazvati streamom, mora na neki način obavijestiti pretplatnike da su u njega stigli novi podaci. Tako dolazimo do definicije toka.
Protok je niz podataka sortiranih po vremenu koji može ukazivati da su se podaci promijenili. Sada zamislite koliko je zgodno pisati kod u kojem trebate pokrenuti nekoliko događaja u različitim dijelovima koda za jednu akciju. Jednostavno se pretplatimo na stream i on će nam reći kada dođe do promjena. I RxJs biblioteka to može učiniti.
RxJS je biblioteka za rad sa asinhronim programima i programima zasnovanim na događajima koji koriste vidljive sekvence. Biblioteka pruža glavni tip Posmatrano, nekoliko tipova pomoćnika (Posmatrači, Planeri, Subjekti) i operatori za rad sa događajima kao sa kolekcijama (mapa, filter, redukcija, svaki i slični iz JavaScript niza).
Hajde da razumemo osnovne koncepte ove biblioteke.
Opservable, Observer, Producer
Opservable je prvi osnovni tip koji ćemo pogledati. Ova klasa sadrži glavni dio implementacije RxJs. Pridružen je vidljivom toku, na koji se može pretplatiti korištenjem metode pretplate.
Observable implementira pomoćni mehanizam za kreiranje ažuriranja, tzv posmatrač. Poziva se izvor vrijednosti za promatrača producent. To može biti niz, iterator, web socket, neka vrsta događaja, itd. Dakle, možemo reći da je observable provodnik između Producenta i Observera.
Observable obrađuje tri vrste Observer događaja:
sljedeći - novi podaci
greška - greška ako je niz prekinut zbog izuzetka. ovaj događaj takođe 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. dobijamo 4 i završavamo našu nit.
Razmišljam naglas
A onda sam shvatio da je zanimljivije pričati nego pisati o tome. 😀
pretplata
Kada se pretplatimo na stream, kreiramo novu klasu pretplata, što nam daje mogućnost da otkažemo pretplatu na metodu odjavite se. Također možemo grupisati pretplate koristeći metodu dodati. Pa, logično je da možemo razgrupisati niti koristeći ukloniti. Metode dodavanja i uklanjanja prihvataju drugu pretplatu kao ulaz. Želio bih napomenuti da kada otkažemo pretplatu, poništavamo pretplatu na sve podređene pretplate kao da su oni također pozvali metodu odjave. Nastavi.
Vrste tokova
HOT
HLADNO
Proizvođač je kreiran izvan vidljivog
Proizvođač je kreiran unutar vidljivog
Podaci se prosljeđuju u vrijeme kreiranja promatranog
Podaci se dostavljaju u trenutku pretplate.
Treba više logike da odjavite pretplatu
Nit se sama završava
Koristi odnos jedan prema više
Koristi odnos jedan-na-jedan
Sve pretplate imaju istu vrijednost
Pretplate su nezavisne
Podaci se mogu izgubiti ako nema pretplate
Ponovno izdaje sve vrijednosti toka za novu pretplatu
Da dam analogiju, zamislio bih vrući tok poput filma u bioskopu. U kom trenutku ste došli, od tog trenutka ste počeli da gledate. Uporedio bih hladan tok sa pozivom u njima. podrška. Svaki pozivalac sluša snimak telefonske sekretarice od početka do kraja, ali možete prekinuti vezu ako se odjavite.
Želio bih napomenuti da postoje i takozvani topli tokovi (svaku definiciju sam sreo izuzetno rijetko i samo u stranim zajednicama) - to je tok koji se iz hladnog pretvara u topli. Postavlja se pitanje - gdje koristiti)) Dat ću primjer iz prakse.
Radim sa Angularom. Aktivno koristi rxjs. Da bih dobio podatke na server, očekujem hladan tok i koristim ovaj stream u šablonu koristeći asyncPipe. Ako ovu cev koristim nekoliko puta, onda, vraćajući se na definiciju hladnog toka, svaka cev će tražiti podatke od servera, što je u najmanju ruku čudno. A ako pretvorim hladan tok u topli, tada će se zahtjev dogoditi jednom.
Općenito, razumijevanje vrste tokova je prilično teško za početnike, ali važno.
operatori
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 priliku da radimo sa streamovima. Oni pomažu u kontroli događaja koji teku u Observable-u. Razmotrit ćemo nekoliko najpopularnijih, a više informacija o operaterima možete pronaći na linkovima u korisnim informacijama.
Operateri-of
Počnimo s pomoćnim operatorom. Kreira Observable na osnovu jednostavne vrijednosti.
Operatori-filter
Operator filtera, kao što ime govori, filtrira stream signal. Ako operator vrati true, onda preskače dalje.
Operateri - uzmi
take - Uzima vrijednost broja emisija, nakon čega se tok završava.
Operatori-debounceTime
debounceTime - odbacuje emitirane vrijednosti koje spadaju u određeni vremenski interval između izlaznih podataka - nakon što vremenski interval prođe, emituje 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
Emituje vrijednosti sve dok takeWhile ne vrati false, a zatim otkaže pretplatu na nit.
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 )
);
Kombinacija operatoraNajnoviji
Kombinovani operator kombiLatest je donekle sličan obećanju.all. Kombinira više tokova u jedan. Nakon što je svaka nit napravila barem jedno emitovanje, dobijamo najnovije vrijednosti iz svake kao niz. Nadalje, nakon bilo kakvog emitiranja iz kombinovanih 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 osnovu ovih 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));
Operateri - forkJoin
forkJoin također spaja niti, ali emituje vrijednost samo kada su sve niti završ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);
Operateri-mapa
Operator transformacije mape transformira vrijednost emisije 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)
);
Operateri - podijelite, dodirnite
Operator tap vam omogućava da izvršite nuspojave, odnosno sve radnje koje ne utiču na sekvencu.
Operater za dijeljenje može hladan tok pretvoriti u vrući.
Operateri su gotovi. Pređimo na predmet.
Razmišljam naglas
A onda sam otišao da popijem čaj. Umorna sam od ovih primjera 😀
Predmetna porodica
Porodica predmeta je odličan primjer vrućih niti. Ove klase su neka vrsta hibrida koji istovremeno djeluju i kao promatrač i promatrač. Budući da je predmet vrući stream, morate ga otkazati. Ako govorimo o glavnim metodama, onda su to:
sljedeće - prosljeđivanje novih podataka u stream
greška - greška i prekid niti
kompletan - kraj niti
pretplatite se - pretplatite se na stream
odjaviti se - odjaviti se sa teme
asObservable - transformirajte se u posmatrača
toPromise - pretvara se u obećanje
Odredite 4 5 vrsta predmeta.
Razmišljam naglas
Rekao sam 4 na streamu, ali se pokazalo da su dodali još jednog. Kako se kaže, živi i uči.
Simple Subject new Subject()- najjednostavnije vrste predmeta. Kreirano bez parametara. Prolazi vrijednosti koje su stigle tek nakon pretplate.
BehaviorSubject new BehaviorSubject( defaultData<T> ) - po mom mišljenju najčešći tip predmeta. Ulaz uzima zadanu vrijednost. Uvijek pohranjuje podatke posljednjeg izdanja, 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) - Opciono, može uzeti kao prvi argument veličinu bafera vrijednosti koje će pohraniti u sebe, a drugi put tokom kojeg su nam potrebne promjene.
asyncsubject new AsyncSubject() - ništa se ne događa kada se pretplatite, a vrijednost će biti vraćena tek kada se završi. Biće vraćena samo posljednja vrijednost toka.
WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - Dokumentacija o tome šuti i ja to prvi put vidim. Ko zna čime se bavi, napišite, dodaćemo.
Fuj. Pa, razmotrili smo sve ono što sam danas htio reći. Nadam se da su ove informacije bile od pomoći. Listu literature možete sami pročitati u kartici Korisne informacije.