Nesinkrona programado en JavaScript (Callback, Promise, RxJs)
Saluton al ĉiuj. En kontakto Omelnitsky Sergey. Antaŭ ne longe, mi gastigis fluon pri reaktiva programado, kie mi parolis pri malsinkronio en JavaScript. Hodiaŭ mi ŝatus resumi ĉi tiun materialon.
Sed antaŭ ol komenci la ĉefan materialon, ni devas fari enkondukon. Do ni komencu per difinoj: kio estas stack kaj queue?
Pilo estas kolekto kies elementoj estas prenitaj laŭ "lasta enen, unue eksteren" LIFO-bazo
Kolo estas kolekto, kies elementoj estas akiritaj laŭ la principo (“unua eniranta, unue eliranta” FIFO
Bone, ni daŭrigu.
JavaScript estas unufadena programlingvo. Ĉi tio signifas, ke ĝi havas nur unu fadenon de ekzekuto kaj unu stakon, kie funkcioj estas vicigitaj por ekzekuto. Tial JavaScript povas nur plenumi unu operacion samtempe, dum aliaj operacioj atendos sian vicon sur la stako ĝis ili estos vokataj.
Voka stako estas datumstrukturo kiu, en simplaj terminoj, registras informojn pri la loko en la programo kie ni estas. Se ni saltas en funkcion, ni puŝas ĝian eniron al la supro de la stako. Kiam ni revenas de funkcio, ni elŝprucas la plej supran elementon de la stako kaj finas de kie ni vokis la funkcion. Tion la stako povas fari. Kaj nun tre interesa demando. Kiel do asinkronio funkcias en JavasScript?
Fakte, krom la stako, retumiloj havas specialan voston por labori kun la tiel nomata WebAPI. Funkcioj de ĉi tiu vico estos ekzekutitaj en ordo nur post kiam la stako estas tute malplenigita. Nur post tio ili estas metitaj de la atendovico sur la stakon por ekzekuto. Se estas almenaŭ unu elemento sur la stako nuntempe, tiam ili ne povas atingi la stakon. Ĝuste pro tio, voki funkciojn per tempotempo ofte estas malpreciza en tempo, ĉar la funkcio ne povas veni de la atendovico al la stako dum ĝi estas plena.
Ni rigardu la sekvan ekzemplon kaj ni trarigardu ĝin paŝon post paŝo. Ni vidu ankaŭ kio okazas en la sistemo.
1) Ĝis nun nenio okazas. La retumila konzolo estas pura, la voka stako estas malplena.
2) Tiam la komando console.log('Saluton') estas aldonita al la voka stako.
3) Kaj ĝi estas plenumita
4) Tiam console.log('Saluton') estas forigita de la voka stako.
5) Nun ni transiru al la komando setTimeout(function cb1() {… }). Ĝi estas aldonita al la voka stako.
6) La ordono setTimeout(funkcio cb1() {… }) estas ekzekutita. La retumilo kreas tempigilon kiu estas parto de la Reta API. Ĝi faros retronombradon.
7) La komando setTimeout(function cb1() {… }) finis sian laboron kaj estas forigita el la voka stako.
8) La komando console.log('Bye') estas aldonita al la voka stako.
9) La komando console.log('Bye') estas ekzekutita.
10) La komando console.log('Bye') estas forigita el la voka stako.
11) Post kiam almenaŭ 5000ms pasis, la tempigilo finiĝas kaj metas la cb1-revokon en la revokvicon.
12) La okazaĵa buklo prenas funkcion cb1 de la revokatvico kaj puŝas ĝin sur la voka stako.
13) La cb1 funkcio estas ekzekutita kaj aldonas console.log('cb1') al la voka stako.
14) La komando console.log('cb1') estas ekzekutita.
15) La komando console.log('cb1') estas forigita de la voka stako.
16) Funkcio cb1 estas forigita de la voka stako.
Ni rigardu ekzemplon en dinamiko:
Nu, ni rigardis kiel malsinkronio estas efektivigita en JavaScript. Nun ni parolu mallonge pri la evoluo de nesinkrona kodo.
La evoluo de nesinkrona kodo.
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);
})
})
})
})
})
});
Nesinkrona programado kiel ni konas ĝin en JavaScript povas esti farita nur per funkcioj. Ili povas esti transdonitaj kiel ajna alia variablo al aliaj funkcioj. Tiel naskiĝis revokoj. Kaj ĝi estas mojosa, amuza kaj fervora, ĝis ĝi fariĝas malĝojo, melankolio kaj malĝojo. Kial? Jes, ĝi estas simpla:
Dum la komplekseco de la kodo kreskas, la projekto rapide iĝas malklaraj multoblaj nestitaj blokoj - "callback hell".
Erartraktado povas esti facile preteratentita.
Vi ne povas redoni esprimojn kun return.
Kun la alveno de Promeso, la situacio iom pliboniĝis.
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);
});
Aperis promesĉenoj, kiuj plibonigis la legeblecon de la kodo
Estis aparta metodo de interkapto de eraroj
Paralela ekzekuto kun Promise.all aldonis
Ni povas solvi nestitan asinkronion per async/wait
Sed la promeso havas siajn limojn. Ekzemple, promeso, sen dancado per tamburino, ne povas esti nuligita, kaj plej grave, ĝi funkcias kun unu valoro.
Nu, ĉi tie ni glate alproksimiĝas al reaktiva programado. Laca? Nu, la bona afero estas, ke vi povas iri fari kelkajn mevojn, cerbumi kaj reveni por legi pli. Kaj mi daŭrigos.
Reaktiva programado - programa paradigmo fokusita al datumfluoj kaj disvastigo de ŝanĝoj. Ni rigardu pli detale, kio estas datumfluo.
// Получаем ссылку на элемент
const input = ducument.querySelector('input');
const eventsArray = [];
// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
event => eventsArray.push(event)
);
Ni imagu, ke ni havas enigkampon. Ni kreas tabelon, kaj por ĉiu klavo de la eniga evento, ni stokos la eventon en nia tabelo. Samtempe, mi ŝatus noti, ke nia tabelo estas ordigita laŭ tempo, t.e. la indekso de postaj eventoj estas pli granda ol la indekso de pli fruaj. Tia tabelo estas simpligita datumfluomodelo, sed ĝi ankoraŭ ne estas fluo. Por ke ĉi tiu aro estu sekure nomita rivereto, ĝi devas iel povi informi abonantojn, ke novaj datumoj alvenis en ĝi. Tiel ni venas al la difino de fluo.
Fluo estas tabelo de datumoj ordigitaj laŭ tempo kiu povas indiki ke la datumoj ŝanĝiĝis. Nun imagu kiel oportune fariĝas skribi kodon, en kiu vi devas ekigi plurajn eventojn en malsamaj partoj de la kodo por unu ago. Ni simple abonas la fluon kaj ĝi diros al ni kiam okazos ŝanĝoj. Kaj la biblioteko RxJs povas fari tion.
RxJS estas biblioteko por labori kun nesinkronaj kaj okazaĵ-bazitaj programoj uzantaj observeblajn sekvencojn. La biblioteko provizas la ĉefan tipon Observinda, pluraj helptipoj (Observantoj, Planistoj, Subjektoj) kaj funkciigistoj por labori kun eventoj kiel kun kolektoj (mapo, filtri, redukti, ĉiu kaj similaj el JavaScript Array).
Ni komprenu la bazajn konceptojn de ĉi tiu biblioteko.
Observebla, Observanto, Produktanto
Observebla estas la unua baza tipo, kiun ni rigardos. Ĉi tiu klaso enhavas la ĉefan parton de la efektivigo de RxJs. Ĝi estas rilata al observebla rivereto, kiu povas esti abonita uzante la abonmetodon.
Observable efektivigas helpan mekanismon por krei ĝisdatigojn, la tn observanto. La fonto de valoroj por Observanto nomiĝas produktanto. Ĝi povas esti tabelo, iteratoro, interreta ingo, ia evento ktp. Do ni povas diri, ke observebla estas konduktoro inter Produktanto kaj Observanto.
Observebla pritraktas tri specojn de Observer-okazaĵoj:
sekva - novaj datumoj
eraro - eraro se la sinsekvo finiĝis pro escepto. ĉi tiu okazaĵo ankaŭ implicas la finon de la sekvenco.
kompleta - signalo pri la fino de la sinsekvo. Ĉi tio signifas, ke ne estos pli novaj datumoj
Ni vidu demonstraĵon:
Komence ni prilaboros la valorojn 1, 2, 3, kaj post 1 sek. ni ricevas 4 kaj finas nian fadenon.
Pensante laŭte
Kaj tiam mi konstatis, ke estas pli interese rakonti ol skribi pri tio. 😀
abono
Kiam ni abonas al rivereto, ni kreas novan klason abono, kiu donas al ni la eblon malaboni per la metodo malaboni. Ni ankaŭ povas grupigi abonojn uzante la metodon aldoni. Nu, estas logike, ke ni povas malgrupigi fadenojn uzante forigu. La metodoj aldoni kaj forigi akceptas malsaman abonon kiel enigon. Mi ŝatus rimarki, ke kiam ni malaboniĝas, ni malaboniĝas de ĉiuj infanaj abonoj kvazaŭ ili ankaŭ nomis la malaboni metodo. Antaŭeniri.
Tipoj de riveretoj
HOT
MALVARMA
Produktanto estas kreita ekster la observebla
Produktanto estas kreita ene observebla
Datenoj estas pasigitaj kiam la observebla estas kreita
Datumoj estas provizitaj en la momento de abono.
Bezonas pli da logiko por malaboni
Fadeno finiĝas memstare
Uzas unu-al-multajn rilaton
Uzas unu-al-unu rilaton
Ĉiuj abonoj havas la saman valoron
Abonoj estas sendependaj
Datumoj povas esti perditaj se ne ekzistas abono
Reeldonas ĉiujn fluajn valorojn por nova abono
Por doni analogion, mi imagus varman fluon kiel filmon en kinejo. En kiu momento vi venis, de tiu momento vi komencis rigardi. Mi komparus malvarman fluon kun voko en tiuj. subteno. Ĉiu alvokanto aŭskultas la registrilon de la respondilo de la komenco ĝis la fino, sed vi povas haltigi per malabono.
Mi ŝatus rimarki, ke ekzistas ankaŭ tiel nomataj varmaj riveretoj (tian difinon mi renkontis ege malofte kaj nur en fremdaj komunumoj) - tio estas rivereto, kiu transformiĝas de malvarma rivereto en varman. Estiĝas la demando - kie uzi)) Mi donos ekzemplon el praktiko.
Mi laboras kun Angular. Li aktive uzas rxjs. Por ricevi datumojn al la servilo, mi atendas malvarman fluon kaj mi uzas ĉi tiun fluon en la ŝablono uzante asyncPipe. Se mi plurfoje uzas ĉi tiun tubon, do, revenante al la difino de malvarma fluo, ĉiu tubo petos datumojn de la servilo, kio estas stranga por diri. Kaj se mi konvertas malvarman rivereton al varma, tiam la peto okazos unufoje.
Ĝenerale, kompreni la tipon de fluoj estas sufiĉe malfacila por komencantoj, sed grava.
Telefonistoj
return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
.pipe(
tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
map(({ data }: TradeCompanyList) => data)
);
Operaciantoj donas al ni ŝancon labori kun riveretoj. Ili helpas kontroli la eventojn fluantajn en la Observebla. Ni konsideros kelkajn el la plej popularaj, kaj pli da informoj pri la telefonistoj troveblas ĉe la ligiloj en utilaj informoj.
Operatoroj-de
Ni komencu per la helpanto operatoro de. Ĝi kreas Observeblan bazitan sur simpla valoro.
Operatoroj-filtrilo
La filtrila funkciigisto, kiel la nomo implicas, filtras la fluosignalon. Se la funkciigisto resendas vera, tiam ĝi saltas plu.
Operatoroj - prenu
take - Prenas la valoron de la nombro da elsendaĵoj, post kio la rivereto finiĝas.
Operatoroj-debounceTime
debounceTime - forĵetas elsenditajn valorojn kiuj falas ene de la specifita tempointervalo inter eligo-datumoj - post kiam la tempointervalo pasis, elsendas la lastan valoron.
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)
);
Operatoroj-takeWhile
Elsendas valorojn ĝis takeWhile revenas falsa, tiam malaboniĝas de la rivereto.
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 )
);
Operatoroj-kombiniLastajn
La kombinita operatoro combineLatest estas iom simila al promise.all. Ĝi kombinas plurajn fluojn en unu. Post kiam ĉiu fadeno faris almenaŭ unu elsendon, ni ricevas la plej novajn valorojn de ĉiu kiel tabelo. Plue, post ajna elsendo de la kombinitaj fluoj, ĝi donos novajn valorojn.
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));
Operatoroj-zip
Zip - atendas valoron de ĉiu fluo kaj formas tabelon bazitan sur ĉi tiuj valoroj. Se la valoro ne venas de iu fadeno, tiam la grupo ne estos formita.
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));
Operatoroj - forkJoin
forkJoin ankaŭ kunligas fadenojn, sed ĝi nur elsendas valoron kiam ĉiuj fadenoj estas kompletaj.
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);
Operatoroj-mapo
La mapa transformfunkciigisto transformas la elsendi valoron en novan.
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)
);
Funkciistoj - kunhavigi, frapeti
La frapeta operatoro permesas vin fari kromefikojn, tio estas, ajnajn agojn, kiuj ne influas la sekvencon.
La akcia servaĵofunkciigisto povas turni malvarman fluon en varman.
Operaciantoj estas faritaj. Ni transiru al Temo.
Pensante laŭte
Kaj poste mi iris trinki teon. Mi estas laca de ĉi tiuj ekzemploj 😀
Subjekta familio
La subjekta familio estas ĉefa ekzemplo de varmaj fadenoj. Ĉi tiuj klasoj estas speco de hibridoj kiuj funkcias kiel observebla kaj observanto samtempe. Ĉar la temo estas varma fluo, ĝi devas esti malabonita de. Se ni parolas pri la ĉefaj metodoj, tiam ĉi tiuj estas:
sekva - pasi novajn datumojn al la rivereto
eraro - eraro kaj fadenfino
kompleta - fino de la fadeno
subscribe - aboni al rivereto
unsubscribe - malaboni de la rivereto
asObservable - transformiĝi en observanton
toPromise - transformiĝas en promeson
Asignu 4 5 specojn de temoj.
Pensante laŭte
Mi diris 4 sur la rivereto, sed montriĝis, ke ili aldonis unu plian. Kiel oni diras, vivu kaj lernu.
Simpla Temo new Subject()- la plej simpla speco de temoj. Kreita sen parametroj. Transdonas la valorojn, kiuj venis nur post la abono.
KondutoSubjekto new BehaviorSubject( defaultData<T> ) - miaopinie la plej ofta speco de subjekto-j. La enigo prenas la defaŭltan valoron. Ĉiam konservas la datumojn de la lasta numero, kiu estas transdonita dum abono. Ĉi tiu klaso ankaŭ havas utilan valormetodon kiu resendas la nunan valoron de la rivereto.
ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) - Laŭvole, ĝi povas preni kiel la unuan argumenton la grandecon de la bufro de valoroj, kiun ĝi stokos en si mem, kaj la duan fojon dum kiu ni bezonas ŝanĝojn.
nesinkrona subjekto new AsyncSubject() - nenio okazas dum abonado, kaj la valoro estos redonita nur kiam kompleta. Nur la lasta valoro de la fluo estos resendita.
WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - La dokumentaro silentas pri ĝi kaj mi mem vidas ĝin unuafoje. Kiu scias, kion li faras, skribu, ni aldonos.
Huf. Nu, ni pripensis ĉion, kion mi volis rakonti hodiaŭ. Mi esperas, ke ĉi tiu informo estis helpema. Vi povas legi la liston de literaturo memstare en la langeto Utilaj Informoj.