Programimi asinkron në JavaScript (Callback, Promise, RxJs)
Pershendetje te gjitheve. Sergej Omelnitsky është në kontakt. Jo shumë kohë më parë unë prita një transmetim mbi programimin reaktiv, ku fola për asinkroninë në JavaScript. Sot dua të mbaj shënime për këtë material.
Por, para se të fillojmë materialin kryesor, duhet të bëjmë një shënim hyrës. Pra, le të fillojmë me përkufizimet: çfarë është një pirg dhe një radhë?
Biftek është një koleksion, elementët e të cilit janë marrë në bazë të LIFO-s me hyrjen e fundit dhe të parën
Radhe është një koleksion, elementët e të cilit janë marrë në bazë të FIFO-së me hyrjen e parë, të parën
Mirë, le të vazhdojmë.
JavaScript është një gjuhë programimi me një fije të vetme. Kjo do të thotë se ekziston vetëm një thread ekzekutimi dhe një stack në të cilin funksionet janë në radhë për ekzekutim. Prandaj, JavaScript mund të kryejë vetëm një operacion në të njëjtën kohë, ndërsa operacionet e tjera do të presin radhën e tyre në pirg derisa të thirren.
Stafi i thirrjeve është një strukturë të dhënash që, thënë thjesht, regjistron informacione për vendin në programin ku ndodhemi. Nëse kalojmë në një funksion, e shtyjmë hyrjen e tij në krye të pirgut. Kur kthehemi nga një funksion, nxjerrim elementin më të lartë nga grumbulli dhe përfundojmë aty ku kemi thirrur funksionin. Kjo është gjithçka që mund të bëjë pirgu. Dhe tani një pyetje jashtëzakonisht interesante. Atëherë, si funksionon asinkronia në JavasScript?
Në fakt, përveç stakut, shfletuesit kanë një radhë të veçantë për të punuar me të ashtuquajturin WebAPI. Funksionet në këtë radhë do të ekzekutohen sipas radhës vetëm pasi pirgja të jetë pastruar plotësisht. Vetëm pas kësaj ata shtyhen nga radha në pirg për ekzekutim. Nëse ka të paktën një element në pirg për momentin, atëherë ata nuk mund të shtohen në pirg. Pikërisht për shkak të kësaj, thirrja e funksioneve me kohëzgjatje shpesh nuk është e saktë në kohë, pasi funksioni nuk mund të shkojë nga radha në pirg ndërsa është plot.
Le të shohim shembullin e mëposhtëm dhe të fillojmë me zbatimin e tij hap pas hapi. Le të shohim gjithashtu se çfarë ndodh në sistem.
1) Asgjë nuk po ndodh ende. Konsola e shfletuesit është e qartë, grupi i thirrjeve është bosh.
2) Pastaj komanda console.log('Hi') shtohet në grupin e thirrjeve.
3) Dhe është përmbushur
4) Pastaj console.log ('Hi') hiqet nga grupi i thirrjeve.
5) Tani kaloni te komanda setTimeout(funksioni cb1() {… }). Ai shtohet në grupin e thirrjeve.
6) Komanda setTimeout(funksioni cb1() {… }) është ekzekutuar. Shfletuesi krijon një kohëmatës që është pjesë e Web API. Do të kryejë një numërim mbrapsht.
7) Komanda setTimeout(funksioni cb1() {... }) ka përfunduar punën e saj dhe është hequr nga grupi i thirrjeve.
8) Komanda console.log ('Bye') shtohet në grupin e thirrjeve.
9) Komanda console.log('Bye') është ekzekutuar.
10) Komanda console.log ('Bye') hiqet nga grupi i thirrjeve.
11) Pasi të kenë kaluar të paktën 5000 ms, kohëmatësi përfundon dhe vendos kthimin e thirrjes cb1 në radhën e kthimit të thirrjes.
12) Cikli i ngjarjes merr funksionin cb1 nga radha e kthimit të thirrjes dhe e vendos atë në pirgun e thirrjeve.
13) Funksioni cb1 ekzekutohet dhe shton console.log('cb1') në grupin e thirrjeve.
14) Komanda console.log('cb1') është ekzekutuar.
15) Komanda console.log('cb1') hiqet nga grupi i thirrjeve.
16) Funksioni cb1 hiqet nga grupi i thirrjeve.
Le të shohim një shembull në dinamikë:
Epo, ne shikuam se si zbatohet asinkronia në JavaScript. Tani le të flasim shkurtimisht për evolucionin e kodit asinkron.
Evolucioni i kodit asinkron.
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);
})
})
})
})
})
});
Programimi asinkron siç e njohim ne në JavaScript mund të zbatohet vetëm nga funksionet. Ato mund të kalohen si çdo variabël tjetër në funksione të tjera. Kështu lindën thirrjet. Dhe është e lezetshme, argëtuese dhe lozonjare, derisa të kthehet në trishtim, melankoli dhe trishtim. Pse? Është e thjeshtë:
Ndërsa kompleksiteti i kodit rritet, projekti kthehet shpejt në blloqe të errëta, të mbivendosura në mënyrë të përsëritur - "ferri i kthimit të thirrjes".
Trajtimi i gabimeve mund të jetë i lehtë për t'u humbur.
Ju nuk mund të ktheni shprehje me kthim.
Me ardhjen e Premtimit, situata u bë pak më e mirë.
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);
});
U shfaqën zinxhirë premtimesh, të cilat përmirësonin lexueshmërinë e kodit
Është shfaqur një metodë e veçantë për kapjen e gabimeve
U shtua mundësia e ekzekutimit paralel duke përdorur Promise.all
Ne mund të zgjidhim asinkroninë e mbivendosur duke përdorur asinkronizim/prit
Por premtimet kanë kufizimet e tyre. Për shembull, një premtim nuk mund të anulohet pa kërcyer me një dajre, dhe më e rëndësishmja është se ai funksionon me një vlerë.
Epo, ne i kemi afruar pa probleme programimit reaktiv. E lodhur? Epo, për fat të mirë, mund të shkoni të bëni një çaj, të mendoni për këtë dhe të ktheheni për të lexuar më shumë. Dhe unë do të vazhdoj.
Programimi reaktiv është një paradigmë programimi e fokusuar në rrjedhat e të dhënave dhe përhapjen e ndryshimeve. Le të hedhim një vështrim më të afërt se çfarë është një rrjedhë e të dhënave.
// Получаем ссылку на элемент
const input = ducument.querySelector('input');
const eventsArray = [];
// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
event => eventsArray.push(event)
);
Le të imagjinojmë se kemi një fushë hyrëse. Ne po krijojmë një grup dhe për çdo tastierë të ngjarjes hyrëse do ta ruajmë ngjarjen në grupin tonë. Në të njëjtën kohë, dua të vërej se grupi ynë është i renditur sipas kohës, d.m.th. indeksi i ngjarjeve të mëvonshme është më i madh se indeksi i ngjarjeve të mëparshme. Një grup i tillë është një model i thjeshtuar i një rrjedhe të dhënash, por nuk është ende një rrjedhë. Në mënyrë që ky grup të quhet në mënyrë të sigurt një transmetim, ai duhet të jetë në gjendje të informojë disi pajtimtarët se të dhënat e reja kanë mbërritur në të. Kështu vijmë te përkufizimi i rrjedhës.
Rrjedh është një grup të dhënash të renditura sipas kohës që mund të tregojnë se të dhënat kanë ndryshuar. Tani imagjinoni sa i përshtatshëm bëhet të shkruhet kodi në të cilin një veprim kërkon thirrjen e disa ngjarjeve në pjesë të ndryshme të kodit. Ne thjesht abonohemi në transmetim dhe ai do të na njoftojë kur të ndodhin ndryshime. Dhe biblioteka RxJs mund ta bëjë këtë.
RxJS është një bibliotekë për të punuar me programe asinkrone dhe të bazuara në ngjarje duke përdorur sekuenca të vëzhgueshme. Biblioteka ofron një lloj bazë E vëzhgueshme, disa lloje ndihmëse (Vëzhguesi, Planifikuesit, Subjektet) dhe operatorët për të punuar me ngjarjet si me koleksionet (hartë, filtro, redukto, çdo dhe të ngjashme nga JavaScript Array).
Le të kuptojmë konceptet bazë të kësaj biblioteke.
I vëzhgueshëm, vëzhgues, prodhues
Observable është lloji i parë bazë që do të shikojmë. Kjo klasë përmban pjesën kryesore të zbatimit të RxJs. Ai shoqërohet me një rrjedhë të vëzhgueshme, e cila mund të abonohet duke përdorur metodën e abonimit.
Observable zbaton një mekanizëm ndihmës për krijimin e përditësimeve, të ashtuquajturat Mbikëqyrës. Burimi i vlerave për Observer quhet Prodhues. Ky mund të jetë një grup, përsëritës, prizë në internet, një lloj ngjarjeje, etj. Pra, mund të themi se e vëzhgueshme është një përcjellës ndërmjet Prodhuesit dhe Vëzhguesit.
Observable trajton tre lloje të ngjarjeve të Observer:
tjetër - të dhëna të reja
gabim - një gabim nëse sekuenca ka përfunduar për shkak të një përjashtimi. kjo ngjarje nënkupton edhe plotësimin e sekuencës.
i plotë - sinjal për përfundimin e sekuencës. Kjo do të thotë se nuk do të ketë më të dhëna të reja.
Le të shohim demonstrimin:
Në fillim do të përpunojmë vlerat 1, 2, 3 dhe pas 1 sekonde. ne do të marrim 4 dhe do t'i japim fund transmetimit tonë.
Duke menduar me zë të lartë
Dhe atëherë kuptova se të tregoja ishte më interesante sesa të shkruaje për të. 😀
Abonim
Kur abonohemi në një transmetim, krijojmë një klasë të re abonime cila na jep mundësinë të çregjistrohemi duke përdorur metodën unsubscribe. Ne gjithashtu mund të grupojmë abonimet duke përdorur metodën shtoj. Epo, është logjike që ne mund t'i çgrupojmë temat duke përdorur heq. Metodat e shtimit dhe heqjes pranojnë një abonim tjetër si hyrje. Dëshiroj të vërej se kur çregjistrohemi, ne çregjistrohemi nga të gjitha abonimet e fëmijëve sikur të kishin quajtur metodën e çregjistrimit. Shkoni përpara.
Llojet e përrenjve
HOT
FTOH
Prodhuesi është krijuar jashtë të vëzhgueshme
Prodhuesi është krijuar brenda observable
Të dhënat transferohen në kohën kur krijohet e vëzhgueshme
Të dhënat jepen në momentin e abonimit
Duhet logjikë shtesë për çregjistrimin
Fillimi përfundon vetë
Përdor një marrëdhënie një me shumë
Përdor një marrëdhënie një me një
Të gjitha abonimet kanë të njëjtin kuptim
Abonimet janë të pavarura
Të dhënat mund të humbasin nëse nuk keni një abonim
Riboton të gjitha vlerat e transmetimit për një abonim të ri
Për të dhënë një analogji, do të mendoja për një transmetim të nxehtë si një film në një teatër. Në cilën pikë kohore keni mbërritur, që nga ai moment keni filluar të shikoni. Unë do të krahasoja një rrjedhë të ftohtë me një telefonatë në teknologji. mbështetje. Çdo telefonues dëgjon regjistrimin e postës zanore nga fillimi në fund, por ju mund ta mbyllni duke përdorur çabonimin.
Dua të vërej se ekzistojnë edhe të ashtuquajturat rrjedha të ngrohta (këtë përkufizim e kam hasur jashtëzakonisht rrallë dhe vetëm në komunitetet e huaja) - kjo është një rrjedhë që shndërrohet nga një rrjedhë e ftohtë në një të nxehtë. Shtrohet pyetja - ku të përdoret)) Unë do të jap një shembull nga praktika.
Unë jam duke punuar me Angular. Ai përdor në mënyrë aktive rxjs. Për të marrë të dhëna në server, pres një fije të ftohtë dhe e përdor këtë fill në shabllon duke përdorur asyncPipe. Nëse e përdor këtë tub disa herë, atëherë, duke u rikthyer në përkufizimin e një rryme të ftohtë, çdo tub do të kërkojë të dhëna nga serveri, gjë që është e çuditshme për të thënë të paktën. Dhe nëse shndërroj një rrjedhë të ftohtë në një të ngrohtë, atëherë kërkesa do të ndodhë një herë.
Në përgjithësi, të kuptuarit e llojit të flukseve është mjaft e vështirë për fillestarët, por e rëndësishme.
Operatorët
return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
.pipe(
tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
map(({ data }: TradeCompanyList) => data)
);
Operatorët na ofrojnë mundësinë për të zgjeruar aftësinë tonë për të punuar me transmetime. Ato ndihmojnë në kontrollin e ngjarjeve që ndodhin në Observable. Ne do të shikojmë disa nga më të njohurit, dhe më shumë detaje rreth operatorëve mund të gjenden duke përdorur lidhjet në informacionin e dobishëm.
Operatorët - të
Le të fillojmë me operatorin ndihmës të. Ai krijon një të vëzhgueshme bazuar në një vlerë të thjeshtë.
Operatorët - filtër
Operatori i filtrit, siç sugjeron emri, filtron sinjalin e transmetimit. Nëse operatori kthen true, ai kalon më tej.
Operatorët - marrin
take — Merr vlerën e numrit të emetuesve, pas së cilës filli mbaron.
Operatorët - debounceTime
debounceTime - hedh poshtë vlerat e emetuara që bien brenda intervalit kohor të specifikuar midis daljeve - pasi të ketë kaluar intervali kohor, lëshon vlerën e fundit.
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)
);
Operatorët - takeWhile
Emeton vlera derisa takeWhile të kthejë false, pas së cilës çabonohet nga thread.
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 )
);
Operatorët - kombinojnë më të fundit
Operatori combinationLatest është disi i ngjashëm me premtimin.all. Ai kombinon fije të shumta në një. Pasi çdo thread bën të paktën një emetim, ne marrim vlerat më të fundit nga secila në formën e një grupi. Më tej, pas çdo emetimi nga rrjedhat e bashkuara, ai do të japë vlera të reja.
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));
Operatorët - zip
Zip - Pret një vlerë nga çdo thread dhe formon një grup bazuar në këto vlera. Nëse vlera nuk vjen nga ndonjë thread, atëherë grupi nuk do të formohet.
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));
Operatorët - forkJoin
forkJoin gjithashtu bashkon threads, por ai lëshon një vlerë vetëm kur të gjitha threads janë të plota.
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);
Operatorët - hartë
Operatori i transformimit të hartës e transformon vlerën e emetuesit në një të re.
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)
);
Operatorët – ndajeni, prekni
Operatori i rubinetit ju lejon të bëni efekte anësore, domethënë çdo veprim që nuk ndikon në sekuencën.
Operatori i shërbimeve të përbashkëta mund të kthejë një rrymë të ftohtë në një të nxehtë.
Kemi mbaruar me operatorët. Le të kalojmë te Subjekti.
Duke menduar me zë të lartë
Dhe pastaj shkova për të pirë një çaj. Jam lodhur nga këta shembuj 😀
Familja lëndore
Familja lëndore është një shembull kryesor i rrjedhave të nxehta. Këto klasa janë një lloj hibridi që veprojnë në të njëjtën kohë si të vëzhgueshme dhe vëzhguese. Meqenëse tema është një temë e nxehtë, është e nevojshme të çregjistroheni prej saj. Nëse flasim për metodat kryesore, atëherë këto janë:
tjetër - transferimi i të dhënave të reja në transmetim
gabim - gabim dhe përfundimi i fillit
i plotë – plotësimi i fillit
abonohuni - abonohuni në një transmetim
çregjistrohu - çregjistrohu nga transmetimi
as e vëzhgueshme - shndërrohet në vëzhgues
toPromise - shndërrohet në premtim
Ekzistojnë 4 5 lloje lëndësh.
Duke menduar me zë të lartë
Ishin 4 persona që flisnin në rrymë, por doli që ata shtuan edhe një. Siç thonë ata, jetoni dhe mësoni.
Subjekt i thjeshtë new Subject()– Lloji më i thjeshtë i lëndëve. Krijuar pa parametra. Transmeton vlerat e marra vetëm pas abonimit.
SjelljaSubjekti new BehaviorSubject( defaultData<T> ) – për mendimin tim, lloji më i zakonshëm i lëndës. Hyrja merr vlerën e paracaktuar. Ruan gjithmonë të dhënat e numrit të fundit, të cilat transmetohen gjatë abonimit. Kjo klasë ka gjithashtu një metodë të vlerës së dobishme, e cila kthen vlerën aktuale të rrymës.
ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) — Hyrja mund të marrë opsionalisht si argument të parë madhësinë e buferit të vlerave që do të ruajë në vetvete dhe si të dytë kohën gjatë së cilës na duhen ndryshime.
AsyncSubject new AsyncSubject() - asgjë nuk ndodh kur abonoheni dhe vlera do të kthehet vetëm kur të përfundojë. Vetëm vlera e fundit e transmetimit do të kthehet.
WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) — Dokumentacioni hesht për të dhe e shoh për herë të parë. Nëse dikush e di se çfarë bën ai, ju lutemi të shkruani dhe ne do ta shtojmë.
Phew. Epo, ne kemi mbuluar gjithçka që doja t'ju tregoja sot. Shpresoj se ky informacion ishte i dobishëm. Ju mund ta lexoni vetë listën e referencave në skedën e informacionit të dobishëm.