ProHoster > Blog > podávání > Asynchronní programování v JavaScriptu (Callback, Promise, RxJs)
Asynchronní programování v JavaScriptu (Callback, Promise, RxJs)
Ahoj všichni. V kontaktu s Omelnitským Sergejem. Není to tak dávno, co jsem hostoval stream o reaktivním programování, kde jsem mluvil o asynchronii v JavaScriptu. Dnes bych tento materiál rád shrnul.
Ale než se pustíme do hlavního materiálu, musíme udělat úvod. Začněme tedy definicemi: co je zásobník a fronta?
Zásobník je kolekce, jejíž prvky jsou získávány na základě LIFO „poslední dovnitř, první ven“.
Fronta je kolekce, jejíž prvky jsou získávány podle principu („first in, first out“ FIFO
Dobře, pokračujme.
JavaScript je jednovláknový programovací jazyk. To znamená, že má pouze jedno vlákno provádění a jeden zásobník, kde jsou funkce řazeny do fronty ke spuštění. JavaScript tedy může provádět vždy pouze jednu operaci, zatímco ostatní operace počkají, až na ně přijde řada, dokud nebudou vyvolány.
Zásobník hovorů je datová struktura, která zjednodušeně řečeno zaznamenává informace o místě v programu, kde se nacházíme. Pokud skočíme do funkce, posuneme její vstup na vrchol zásobníku. Když se vrátíme z funkce, vyjmeme nejvyšší prvek ze zásobníku a skončíme tam, odkud jsme tuto funkci zavolali. To je vše, co zásobník dokáže. A teď velmi zajímavá otázka. Jak potom funguje asynchronie v JavaScriptu?
Ve skutečnosti mají prohlížeče kromě zásobníku speciální frontu pro práci s tzv. WebAPI. Funkce z této fronty budou provedeny v pořadí až po úplném vymazání zásobníku. Teprve poté jsou umístěny z fronty do zásobníku k provedení. Pokud je na hromádce v tuto chvíli alespoň jeden prvek, nemohou se na hromádku dostat. Právě kvůli tomu je volání funkcí podle časového limitu často časově nepřesné, protože funkce se nemůže dostat z fronty do zásobníku, dokud je plný.
Podívejme se na následující příklad a pojďme si ho projít krok za krokem. Podívejme se také, co se děje v systému.
1) Zatím se nic neděje. Konzole prohlížeče je čistá, zásobník hovorů je prázdný.
2) Poté je do zásobníku volání přidán příkaz console.log('Hi').
3) A je splněno
4) Poté se console.log('Hi') odstraní ze zásobníku volání.
5) Nyní přejdeme k příkazu setTimeout(funkce cb1() {… }). Je přidán do zásobníku volání.
6) Provede se příkaz setTimeout(funkce cb1() {… }). Prohlížeč vytvoří časovač, který je součástí webového rozhraní API. Provede odpočítávání.
7) Příkaz setTimeout(funkce cb1() {… }) dokončil svou práci a je odstraněn ze zásobníku volání.
8) Příkaz console.log('Bye') je přidán do zásobníku volání.
9) Provede se příkaz console.log('Nashledanou').
10) Příkaz console.log('Bye') je odstraněn ze zásobníku volání.
11) Po uplynutí alespoň 5000 ms časovač skončí a zařadí zpětné volání cb1 do fronty zpětných volání.
12) Smyčka událostí převezme funkci cb1 z fronty zpětných volání a vloží ji do zásobníku volání.
13) Funkce cb1 se provede a přidá do zásobníku volání console.log('cb1').
14) Provede se příkaz console.log('cb1').
15) Příkaz console.log('cb1') je odstraněn ze zásobníku volání.
16) Funkce cb1 je odstraněna ze zásobníku volání.
Podívejme se na příklad v dynamice:
No, podívali jsme se na to, jak je asynchronie implementována v JavaScriptu. Pojďme si nyní krátce promluvit o vývoji asynchronního kódu.
Evoluce asynchronního kódu.
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);
})
})
})
})
})
});
Asynchronní programování, jak jej známe v JavaScriptu, lze provádět pouze pomocí funkcí. Mohou být předány jako jakákoli jiná proměnná jiným funkcím. Tak se zrodila zpětná volání. A je to cool, zábavné a vroucí, až se to změní ve smutek, melancholii a smutek. Proč? Ano, je to jednoduché:
Jak roste složitost kódu, projekt se rychle promění v nejasné vícenásobné vnořené bloky – „peklo zpětného volání“.
Ošetření chyb lze snadno přehlédnout.
Nemůžete vrátit výrazy s návratem.
S příchodem Promise se situace trochu zlepšila.
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);
});
Objevily se slibové řetězce, které zlepšily čitelnost kódu
Existovala samostatná metoda zachycení chyb
Paralelní provádění s Promise.all přidáno
Vnořenou asynchronii můžeme vyřešit pomocí async/await
Ale slib má svá omezení. Například slib, bez tance s tamburínou, nelze zrušit a hlavně funguje s jednou hodnotou.
No a tady se plynule blížíme k reaktivnímu programování. Unavený? Dobrá věc je, že si můžete jít uvařit racky, zamyslet se nad tím a vrátit se a přečíst si víc. A budu pokračovat.
Reaktivní programování - programovací paradigma zaměřené na datové toky a šíření změn. Podívejme se blíže na to, co je to datový tok.
// Получаем ссылку на элемент
const input = ducument.querySelector('input');
const eventsArray = [];
// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
event => eventsArray.push(event)
);
Představme si, že máme vstupní pole. Vytvoříme pole a pro každé sepsání vstupní události uložíme událost do našeho pole. Zároveň bych rád poznamenal, že naše pole je řazeno podle času, tzn. index pozdějších událostí je větší než index dřívějších událostí. Takové pole je zjednodušeným modelem toku dat, ale ještě to není tok. Aby se toto pole mohlo bezpečně nazývat stream, musí být schopné nějak informovat předplatitele, že do něj dorazila nová data. Tím se dostáváme k definici proudění.
Tok je pole dat seřazených podle času, které může naznačovat, že se data změnila. Nyní si představte, jak pohodlné je psát kód, ve kterém musíte pro jednu akci spustit několik událostí v různých částech kódu. Jednoduše se přihlásíme k odběru streamu a ten nám řekne, kdy nastanou změny. A knihovna RxJs to umí.
RxJS je knihovna pro práci s asynchronními a událostmi založenými programy pomocí pozorovatelných sekvencí. Knihovna poskytuje hlavní typ Pozorovatelné, několik typů pomocníků (Pozorovatelé, plánovači, subjekty) a operátory pro práci s událostmi jako s kolekcemi (mapa, filtr, zmenšení, každý a podobné z JavaScript Array).
Pojďme pochopit základní koncepty této knihovny.
Pozorovatelný, pozorovatel, producent
Observable je první základní typ, na který se podíváme. Tato třída obsahuje hlavní část implementace RxJs. Je spojen s pozorovatelným tokem, k jehož odběru se lze přihlásit pomocí metody odběru.
Observable implementuje pomocný mechanismus pro vytváření aktualizací, tzv Pozorovatel. Zdroj hodnot pro pozorovatele se nazývá Výrobce. Může to být pole, iterátor, webový soket, nějaký druh události atd. Můžeme tedy říci, že pozorovatelný je vodič mezi Producentem a Observerem.
Observable zpracovává tři druhy událostí pozorovatele:
další - nová data
error - chyba, pokud byla sekvence ukončena z důvodu výjimky. tato událost také znamená konec sekvence.
kompletní - signál o konci sekvence. To znamená, že již nebudou žádná nová data
Podívejme se na ukázku:
Na začátku zpracujeme hodnoty 1, 2, 3 a po 1 sec. dostaneme 4 a ukončíme naše vlákno.
Přemýšlet nahlas
A pak jsem si uvědomil, že je zajímavější vyprávět, než o tom psát. 😀
Předplatné
Když se přihlásíme k odběru streamu, vytvoříme novou třídu předplatné, což nám dává možnost odhlásit se s metodou odhlásit. Pomocí metody můžeme také seskupovat odběry přidat. Je logické, že můžeme rozdělit vlákna pomocí odstranit. Metody přidání a odebrání přijímají jako vstup jiné předplatné. Chtěl bych poznamenat, že když se odhlásíme, odhlásíme se ze všech dětských odběrů, jako by také volali metodu odhlášení. Pokračuj.
Typy proudů
HOT
STUDENÝ
Producent je vytvořen mimo pozorovatelné
Producent je vytvořen uvnitř pozorovatelny
Data jsou předávána v okamžiku vytvoření pozorovatelného prvku
Údaje jsou poskytovány v době předplatného.
K odhlášení potřebujete více logiky
Vlákno se samo ukončí
Používá vztah jeden k mnoha
Používá vztah jedna ku jedné
Všechna předplatná mají stejnou hodnotu
Předplatné jsou nezávislé
Pokud neexistuje žádné předplatné, může dojít ke ztrátě dat
Znovu vydá všechny hodnoty streamu pro nové předplatné
Abych to přirovnal, představoval bych si horký stream jako film v kině. V jakém okamžiku jste přišli, od toho okamžiku jste se začali dívat. Studený proud bych přirovnal k volání v těch. Podpěra, podpora. Každý volající poslouchá záznam záznamníku od začátku do konce, ale můžete zavěsit a odhlásit se z odběru.
Podotýkám, že existují i tzv. teplé proudy (s takovou definicí jsem se setkal extrémně zřídka a pouze v cizích komunitách) - jedná se o proud přecházející ze studeného proudu na horký. Nabízí se otázka - kde použít)) Uvedu příklad z praxe.
Pracuji s Angular. Aktivně používá rxjs. Pro získání dat na server očekávám studený stream a tento stream používám v šabloně pomocí asyncPipe. Pokud tuto rouru použiji několikrát, pak se vrátím k definici studeného proudu, každá roura bude vyžadovat data ze serveru, což je přinejmenším podivné. A když převedu studený proud na teplý, tak požadavek jednou proběhne.
Obecně platí, že pochopení typu toků je pro začátečníky poměrně obtížné, ale důležité.
Operátoři
return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
.pipe(
tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
map(({ data }: TradeCompanyList) => data)
);
Operátoři nám poskytují možnost pracovat se streamy. Pomáhají kontrolovat události plynoucí v Observable. Zvážíme několik nejoblíbenějších a další informace o operátorech naleznete na odkazech v užitečných informacích.
Provozovatelé
Začněme pomocným operátorem. Vytváří Observable na základě jednoduché hodnoty.
Operátoři-filtr
Operátor filtru, jak název napovídá, filtruje proudový signál. Pokud operátor vrátí hodnotu true, přeskočí dále.
Operátoři - vezměte
take - Bere hodnotu počtu emitů, po kterých stream končí.
Operators-debounceTime
debounceTime - zahodí emitované hodnoty, které spadají do zadaného časového intervalu mezi výstupními daty - po uplynutí časového intervalu vyšle poslední hodnotu.
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)
);
Operátoři-takeWhile
Vysílá hodnoty, dokud takeWhile vrátí false a poté se odhlásí z vlákna.
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 )
);
Operátoři-kombinovatNejnovější
Kombinovaný operátor combLatest je trochu podobný slibu.all. Kombinuje více proudů do jednoho. Poté, co každé vlákno provede alespoň jednu emisi, získáme nejnovější hodnoty z každého jako pole. Dále po každém emitování z kombinovaných toků poskytne nové hodnoty.
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));
Operátory-zip
Zip - čeká na hodnotu z každého streamu a na základě těchto hodnot vytvoří pole. Pokud hodnota nepochází z žádného vlákna, skupina se nevytvoří.
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));
Operátoři - forkJoin
forkJoin také spojuje vlákna, ale vyšle hodnotu pouze tehdy, když jsou všechna vlákna dokončena.
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);
Mapa operátorů
Operátor transformace mapy transformuje emitovanou hodnotu na novou.
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)
);
Operátoři – sdílejte, klepněte
Operátor tap vám umožňuje provádět vedlejší efekty, tedy jakékoli akce, které nemají vliv na sekvenci.
Provozovatel sdílené utility může proměnit studený proud na horký proud.
Operátoři jsou hotoví. Pojďme k předmětu.
Přemýšlet nahlas
A pak jsem šel pít čaj. Už jsem z těch příkladů unavená 😀
Rodina předmětů
Skupina předmětů je ukázkovým příkladem horkých vláken. Tyto třídy jsou jakýmsi hybridem, který funguje jako pozorovatel a pozorovatel zároveň. Vzhledem k tomu, že předmět je žhavý stream, je nutné jej odhlásit. Pokud mluvíme o hlavních metodách, pak jsou to:
další - předání nových dat do streamu
chyba - chyba a ukončení vlákna
kompletní - konec vlákna
odběr - odběr streamu
unsubscribe - odhlásit se ze streamu
asObservable - transformace v pozorovatele
toPromise – promění se ve slib
Přidělte 4 5 typů předmětů.
Přemýšlet nahlas
Na streamu jsem řekl 4, ale ukázalo se, že přidali ještě jednu. Jak se říká, žij a uč se.
Jednoduchý předmět new Subject()- nejjednodušší druh předmětů. Vytvořeno bez parametrů. Předává hodnoty, které přišly až po předplatném.
Předmět chování new BehaviorSubject( defaultData<T> ) - dle mého názoru nejčastější typ předmětů. Vstup má výchozí hodnotu. Vždy ukládá data posledního čísla, která se přenáší při předplatném. Tato třída má také metodu užitečné hodnoty, která vrací aktuální hodnotu proudu.
Přehrát předmět new ReplaySubject(bufferSize?: number, windowTime?: number) - Volitelně může vzít jako první argument velikost vyrovnávací paměti hodnot, které v sobě uloží, a druhý čas, během kterého potřebujeme změny.
asynchronní předmět new AsyncSubject() - při přihlášení k odběru se nic neděje a hodnota bude vrácena až po dokončení. Bude vrácena pouze poslední hodnota streamu.
WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - Dokumentace o tom mlčí a já sám to vidím poprvé. Kdo ví, co dělá, napište, doplníme.
Fuj. Dobře, zvážili jsme všechno, co jsem vám dnes chtěl říct. Doufám, že tyto informace byly užitečné. Seznam literatury si můžete přečíst sami v záložce Užitečné informace.