Asinhrono programiranje u JavaScriptu
Uvod
Pre nego što pređemo na glavnu temu članka, napravićemo kratak osvrt na problematiku sinhronog izvršavanja kodnih instrukcija u JavaScript-u:
- sinhroni pristup u programiranju (kako u JS-u, tako i inače), podrazumeva da se instrukcije izvršavaju onim redosledom koji je naveden u izvornoj datoteci
- u JavaScript-u, takav pristup ne stvara poteškoće u slučaju da se instrukcije izvršavaju velikom brzinom (kada praktično deluje da se skripte izvršavaju trenutno), ali ....
- ukoliko neka pojedinačna instrukcija (odnosno, petlja, poziv funkcije i sl), zahteva više vremena za obradu, tako da se vreme obrade ne može zanemariti - nastaje zastoj
Najprostije rečeno, procesorski zahtevne instrukcije mogu zablokirati izvršavanje ostatka koda, što - osim ukoliko sistem ne omogućava procesiranje korisničkih zahteva na drugi način - svakako predstavlja problem.
U JavaScript-u, asinhroni pristup u izvršavanju instrukcija, omogućava da se pod određenim okolnostima, određeni delovi koda izvršavaju 'donekle paralelno' (u praktičnom smislu - 'istovremeno'), što će biti tema kojom ćemo se pozabaviti u nastavku ....
Asinhrono izvršavanje koda u JS-u uopšteno
Radno okruženje u kome se pokreće JavaScript (eng. JS Runtime Environment) - bilo da je u pitanju neki od JS endžina u browserima ili Node JS - zapravo funkcioniše kao single-threaded aplikacija, to jest, izvršava se preko jednog toka programskih instrukcija (jedne "niti") i koristi tzv. "event loop" za pokretanje i kontrolu izvršavanja instrukcija (nešto više o ovoj tematici, u sledećem odeljku).
Shodno prethodnim konstatacijama, reklo bi se da ne ostaje mesta za pokretanje "ne-sekvencijalnih" tokova izvršavanja programskog koda, ali, ipak - pod određenim okolnostima - JS kod se može izvršavati asinhrono:
- preko metode
setTiemout
- u kom slučaju browser od JS endžina preuzima obavezu da vodi računa o naknadnom pokretanju callback funkcije - preko zahteva za primopredaju podataka sa servera (AJAX, Fetch API i sl) - u kom slučaju server obavlja obradu podataka
- događaji (events) su asinhroni i (za kraj)
- pravo paralelno pokretanje različitih delova koda na strani klijenta (uz međusobnu razmenu poruka), može se postići preko tzv. web workera *
Event loop
Pomenuli smo da izvršavanjem instrukcija u JS-u diriguje "event loop", programska petlja koja pokreće JS kod i (u saradnji sa browserom), vodi računa i o drugim procesima koji su bitni za pravilan prikaz stranice.
Zarad očuvanja preglednosti članka, trudićemo se (ovoga puta) da ne zalazimo previše u tematiku event loop-a (članak je i bez toga dovoljno obiman :)), ali, spomenimo ipak, već na ovom mestu, nekoliko odlika navedene petlje preko koje se JS kod izvršava u browseru:
- petlja se ponavlja određenom učestalošću i proverava: stanje zahteva koji su već pokrenuti, DOM i CSS (a obavlja i osvežavanje prikaza sajta u browseru)
- nove instrukcije ulaze u petlju određenim redom
- ukoliko instrukcije ne zahtevaju previše procesorskog vremena (to jest, ako je ukupno vreme izvršavanja svih instrukcija manje od jednog ciklusa event loop-a), petlja je u stanju da se uredno "vrti ukrug", prima nove instrukcije i obavlja ostale zadatke koje smo pomenuli (u međuvremenu, rešeni zadaci 'izlaze' iz petlje)
- ukoliko neka od instrukcija zahteva toliko vremena za obradu da se ne može uklopiti u ciklus event loop-a (recimo, drastičan primer bi mogla biti
while
ilifor
petlja koja se ponavlja više miliona puta), event loop će se zablokirati, što znači da novi ciklus neće započeti - sve dok se prethodni ciklus ne završi
Poslednja stavka koju smo naveli (pre napomene), često predstavlja veliki problem u praksi, što se lako može uočiti u sledećem primeru:
LINK: Primer sinhrone skripte koja blokira event loop
Po pokretanju skripte (kliknite na dugme "Pokretanje"), vidimo da je tab u browseru 'zabagovan'.
Skripta sama po sebi nije posebno zanimljiva (redundantan i namerno neefikasan način za računanje proseka brojeva od 1 do n, pri čemu je n = 2x109, što praktično znači da će izvršavanje navedenog proračuna .... "potrajati"), ali, pokretanje skripte koju koristimo kao primer, demonstrira sinhronu prirodu event loop-a više nego uspešno.
Ako ste pokrenuli skriptu, verujemo da se nimalo ne dvoumite oko toga da li je tab u browseru blokiran, ali, spomenimo ipak šta su spoljašnje manifestacije unutrašnje zasićenosti zadacima:
- GIF animacija je zaustavljena
- nije moguće kliknuti na dugme #2
- nije moguće birati tekst.
U opštem smislu, vremenski dijagram event loop-a može se prikazati slikovito na sledeći način:
- Ako se instrukcije mogu izvršavati blagovremeno ....
.... petlja funkcioniše uredno i nema zastoja.
Na prvoj slici, operacije vezane za DOM, CSS i ažuriranje stranice, nisu učestvovale u petlji.
Navedene operacije pokreću se po potrebi (onda kada browser proceni da je potrebno osvežiti DOM, CSS ili prikaz nekog od elemenata):
- Kada se pokrene naredba koja se ne uklapa u vremenski okvir jednog ciklusa event loop-a ....
.... ostale naredbe su praktično blokirane!
Zadaci koji su izrazito zahtevni po pitanju procesorskog zauzeća, mogu se rešavati preko web workera, posebno zapisanih skripti koje su u stanju da rasterete glavni thread-a JS endžina (čime ćemo se baviti u poslednjem odeljku).
Međutim, prvo ćemo se pozabaviti uobičajenijim načinima za asinhrono pokretanje koda u JS-u (to jest, zadacima koji nisu "drastični"), pri čemu je sada jasno da i takav kod mora biti efikasan u smislu procesorskog zauzeća.
Struktura primera koje ćemo koristiti u članku
Kada su u pitanju asinhroni zahtevi za primopredaju podataka sa servera, nije redak slučaj da funkcije možemo 'pustiti da rade paralelno', međutim, mnogo češće (što u praktičnom smislu predstavlja glavnu temu članka), funkcije je potrebno nadovezivati (odnosno, "ugnežđavati"), tako da jedna funkcija pokreće drugu preko povratnih poziva - pri čemu pozivanje sledeće funkcije zavisi od uspešnog završetka prethodne.
Korišćenje povratnih poziva u navedenim okolnostima, može napraviti veliku zbrku, čemu ćemo svakako posvetiti pažnju zarad opšte informisanosti, ali, razume se da ćemo najviše pažnje posvetiti savremenijim i elegantnijim rešenjima za zapis povratnih poziva, u vidu promise/then/catch
i async/await
sintakse (preko kojih se znatno povećava preglednost koda).
Pre nego što pređemo na navedene teme, glavno pitanje ....
Kako je uopšte moguće upućivati asinhrone zahteve?
Ako se pitate kako je (uopšte) moguće pokretati asinhrone zahteve o kojima govorimo (AJAX, Fetch API i sl), budući da su JS endžini single-threaded programi, odgovor je u stvari prilično jednostavan i već smo ga dali.
Navedeni zahtevi tipično se rešavaju na udaljenim serverima (na kojima se zapravo obavlja obrada podataka), a preko računara klijenta samo se šalju zahtevi i obrađuju primljeni rezultati (što najčešće nisu procesorski zahtevne operacije i ne opterećuju event loop u iole ozbiljnijoj meri).
Zašto nećemo koristiti konkretne funkcije?
Kao što gornji naslov nagoveštava, u primerima u članku, uglavnom će biti korišćene uprošćene, "šematske" funkcije.
Naravno, prave funkcije za slanje zahteva i prijem podataka, svakako su zanimljive od "izmišljenih", međutim, kompleksnost pravih funkcija je takođe krajnje nezanemarljiva, što bi samo moglo da odmogne pri početnom upoznavanju sa tehnikama koje su tema članka.
Upravo je to razlog zašto ćemo ovoga puta biti praktični, i samo ćemo simulirati izvršavanje funkcija koje dopremaju podatke sa servera, jer (u praktičnom smislu), bez obzira na to šta zapravo rade objekti preko kojih se upućuju asinhroni zahtevi, izvršavanje zahteva ima dve glavne odlike:
- vreme izvršavanja
- rezultat koji se na kraju vraća klijentu
Prosto rečeno, potrebno je neko vreme da se zahtev pošalje serveru, da se zahtev obradi, i da potom server pošalje nazad rezultat, a to je nešto što se sasvim dobro može simulirati preko funkcije setTimeout
.
Što se tiče rezultata - jednostavno ćemo "zažmuriti na jedno oko" i praviti se da operišemo nad podacima koji dolaze "odnekud sa servera" (a zapravo ćemo znati da su podaci zapisani u datoteci koja se nalazi u folderu sa skriptama koje pokrećemo).
Pošto smo sve navedeno razumeli (i pre nego što se posvetimo glavnim temama), pogledajmo i jedan jednostavan primer asinhronog izvršavanja JS koda (koji podseća na primere sa kakvima ste se verovatno već sretali)
Osnovni primer asinhronog izvršavanje JS koda
Za najosnovnije upoznavanje sa asinhronim izvršavanjem instrukcija u JS-u, razmotrićemo šta se dešava kada delove sledećeg koda (koji poruke ispisuje redom):
.... upotrebimo u svojstvu povratnih poziva funkcije setTimeout
:
Po izvršavanju koda ....
.... primećujemo da poruke nisu ispisane redom koji je naveden u skripti, što znači da preko funkcije setTimeout
zaista možemo simulirati slanje asinhronih zahteva (bez ikakvih poteškoća), ali, ono što smo videli su zahtevi čije izvršavanje nije međusobno uslovljeno.
Kao što smo već spomenuli, to često nije slučaj sa backend-om web aplikacija, gde pokretanje određene operacije uglavnom zavisi od uspešnog izvršavanja prethodno pokrenutih operacija.
Na primer:
- ukoliko prijava korisnika nije uspešno obavljena, ne može se pristupati korisničkim podacima (ako je prijava bila uspešna, mogu se tražiti drugi sadržaji)
- ako korisnik nije dodat među odobrene korisnike za određenu chat grupu, dalji zahtevi se obustavljaju (ako je korisnik dodat, prelazi se na sledeću operaciju)
- ako korisnik u datoj grupi ima objavljene sadržaje (poruke, slike ....), mogu se tražiti komentari na date sadržaje (ako sadržaja nema, nema ni komentara) ....
Kao što vidimo (kroz navedene primere i druge primere sa kojima se svakodnevno srećemo), lako je uvideti da postoji potreba za ugnežđavanjem funkcija.
Lako je uvideti potrebu, ali, malo je reći da ugnežđene funkcije ne predstavljaju pregledan i elegantan programski kod ....
Ugnežđeni povratni pozivi ("callback hell")
Da bismo videli kako ugnežđeni povratni pozivi mogu "zagorčati život" programerima (i zašto su programeri sa engleskog govornog područja skovali (ne)popularni termin koji se pojavljuje u naslovu odeljka), pogledaćemo odmah konkretan primer (s tim da ovoga puta u primeru nećemo koristiti ni "fiktivne" funkcije, već, praktično - samo nazive).
U svakom slučaju, izvršavanje navedenih funkcija ne može se više simulirati preko jednostavnog koda, kao malopre:
Umesto prostog 'ređanja instrukcija' - funkcije (praktično) moraju biti ugnežđene.
U slučaju ugnežđavanja samo dve funkcije, gde izvršavanje funkcije #1 traje 'neko vreme' (pri čemu se funkcija #2 neće ni pokretati ako funkcija #1 vrati pogrešan rezultat), možemo koristiti sledeći (pseudo)kod:
Ovakav kod već deluje pomalo "konfuzno", ali, i dalje prilično lako možemo 'pohvatati konce'.
Da pojasnimo: unutar prve funkcije setTimeout
:
.... pokreće se kod:
.... koji prvo proverava da li je funkcija f1
vratila korektan rezultat, a potom se pokreće nova funkcija setTimeout
(preko koje se simulira vreme potrebno za izvršavanje funkcije f2
).
Ne baš "skroz jednostavno", ali - ipak sasvim razumljivo. Međutim, ako je umesto dve, potrebno "ugnezditi" pet funkcija (što i jeste bila prvobitna namera), stvari postaju primetno komplikovanije:
Prikazani kod (i slični kodovi), tipično nateraju manje iskusne programere da 'zakolutaju očima' (a recimo i to da se u praksi sreću konkretni primeri koji mogu biti i kompleksniji od onoga što smo videli).
Iskusniji programeri su obično u stanju da "na mišiće" / iskustvo, isprate pripadnosti delova koda ("ako baš moraju"), međutim ....
Iako raščlanjivanje prethodno prikazanog koda (i sličnih kodova), nekim programerima može predstavljati zanimljivu razbibrigu, * slično Sudoku zagonetkama, Rubikovoj kocki, šahovskim problemima i sl (pod uslovom da je na raspolaganju dovoljno vremena), u praktičnim uslovima, kada se softver razvija - kada je vremena malo, a stresa obično više nego dovoljno - nepotrebne "razbibrige" i "piramidalne šeme" predstavljaju samo 'probleme u najavi' - što svakako treba izbegavati.
Dovoljno je da se slučajno doda (ili, što je mnogo verovatnije - obriše), neka od zagrada, ili neki od zareza, posle čega može nastati poveći problem čije rešavanje može potrajati prilično dugo.
Summa summarum: odavno je zaključeno da ugnežđeni povratni pozivi ne predstavljaju iole elegantan i pregledan programski kod, i stoga se izvesno vreme radilo na iznalaženju boljeg rešenja ....
Pojednostavljivanje callback sintakse preko klase Promise (i metoda then i catch)
Sa verovatno najčuvenijom i najpopularnijom revizijom JavaScript-a, koja se pojavila 2015. godine, * uvedena su između ostalog i "obećanja" (eng. promise(s)) - objekti koji predstavljaju interfejs ka 'vrednostima koje će u nekom trenutku u budućnosti postati dostupne' - nakon čega se rezultat obrade može dalje koristiti u programu.
Pri kreiranju klase koja definiše promis, navode se dve callback funkcije:
- funkcija koja će se izvršavati ukoliko promis uredno obavi posao i vrati očekivani rezultat (funkcija po konvenciji nosi naziv
resolve
) - funkcija koja će se izvršavati ukoliko dođe do greške (funkcija po konvenciji nosi naziv
reject
)
.... što ćemo detaljnije prikazati u narednom odeljku, u kome ćemo početi da se bavimo sintaksom promisa.
Kao i obećanja u realnom životu, 'promisi' ** u JavaScript-u na kraju mogu biti 'ispunjeni' (fulfilled) ili 'neispunjeni' (rejected), a sve dok funkcija koja treba da vrati promis, još uvek obavlja svoj posao, promis je u stanju obrade ('pending')
Opšti princip upotrebe objekata klase Promise
, najlakše je razumeti preko konkretnog primera koji se tiče čitanja podataka iz baze: ukoliko zatražimo podatke (preko promisa), podaci neće biti dostupni "istog trenutka", *** ali, posle kraće obrade, podaci će postati dostupni (ili neće - i to je redovna pojava), i onda - bez obzira na ishod - biće pokrenuta odgovarajuća funkcija povratnog poziva preko koje se može odreagovati u skladu sa okolnostima,
U nastavku, prikazaćemo kako se promisi kreiraju, a potom i kako se koriste.
Kreiranje objekta klase Promise
Osnovni konstruktor klase Promise
prima dva argumenta koji predstavljaju prethodno pomenute funkcije povratnog poziva: funkciju koja će biti izvršena ukoliko je promis uspešno "ispunjen" (funkcija resolve
), i funkciju koja će biti izvršena ukoliko promis nije uspešno obavio zadatak (funkcija reject
):
Za početak, možemo prikazati i nekoliko jednostavnih ("školskih") primera koji koriste prethodnu šemu:
Promis može vratiti (samo) povoljan rezultat u obliku niske ....
.... a može vratiti i objekat:
.... međutim, u praksi, potrebno je (naravno) da promis vrati različit rezultat - u zavisnosti od rezultata obrade, u čemu prethodno prikazani kodovi neće biti od prevelike pomoći (drugim rečima: ni iz daleka nije praktično (a pogotovo nije 'zanimljivo'), da promis uvek vrati isti rezultat :)).
Promis koji vraća odgovarajući rezultat, moguće je definisati preko "uokvirujuće" funkcije (naravno, uz korišćenje obe povratne metode - resolve
i reject
).
Kreiranje promisa unutar funkcije
Kao što smo nagovestili u prethodnom odeljku, prvi pravi primer upotrebe klase Promise
(u kome će se pojaviti i blok za obradu nepovoljnog rezultata), podrazumeva da će promis biti "uokviren" telom funkcije, koja kao parametar sadrži uslov od koga zavisi šta će promis vratiti:
Može delovati pomalo 'konfuzno', to što smo ovoga puta promis 'ugnezdili' unutar funkcije (dok smo u prvom primeru neposredno vezali promis za imenovanu promenljivu (tj. referencu), ali, rezultat je praktično isti (kada se funkcija izvrši, rezultat izvršavanja funkcije je - ništa drugo nego promis).
Ovakav pristup koristimo iz razloga što konstruktor klase Promise
ne može direktno primiti dodatne parametre.
Na kraju, budući da privodimo kraju definiciju klase za promise kakve ćemo koristiti u budućim primerima, dodaćemo i funkciju setTimeout
, preko koje ćemo (kao i do sada), 'simulirati' vreme izvršavanje operacija:
Iako se i dalje "pravimo da ne vidimo", da promis koji smo definisali 'ne radi skoro ništa' (sam po sebi), kreirana funkcija i promis koji predstavlja povratnu vrednost, imaju sve odlike neophodne za razumevanje tematike povezivanja promisa, i to će u ovom članku biti sasvim dovoljno (s tim da ćemo se u nastavku ipak još malo 'potruditi' i oko povratnih vrednosti), a već u sledećem članku, bavićemo se konkretnim Fetch API zahtevima koji su implementirani preko promisa.
Šta promisi treba da vrate u različitim situacijama
Pošto smo zaključili da povratne vrednosti promisa koje smo do sada koristili, nisu "posebno zanimljive", red je da prodiskutujemo o tome šta - u opštem smislu (to jest "inače"), promisi treba da vraćaju.
Videli smo već da je tehnički izvodljivo da promis vrati podatke različitih tipova, i stoga se prirodno postavlja pitanje: šta je to što promis (u najširem kontekstu), treba da vrati preko metode resolve
, a šta preko metode reject
.
U jednostavnim slučajevima (kakve smo već videli), odgovor je takođe sasvim jednostavan i nedvosmislen, ali, u ponešto kompleksnijim slučajevima (recimo, pri radu sa bazama podataka), ne postoje pravila 'uklesana u kamenu' koja važe uvek, što znači da se moramo snalaziti.
Da pojasnimo dodatno: metoda resolve
će (gotovo uvek) vraćati očekivani rezultat (u slučaju uspešne obrade), i samo je pitanje kako ćemo tačno formatirati podatke (koji su pročitani iz baze).
Međutim, vredi se posvetiti pažljivom izboru povratne vrednosti za metodu reject
.
Na primer, promis koji traži korisnika u bazi, mogao bi da vrati grešku ako korisnik nije pronađen (prikazujemo pseudokod):
Sa druge strane, promis koji traži poruke korisnika (npr. u bazi podataka koja se koristi u određenoj chat aplikaciji), najverovatnije ne bi trebalo da aktivira callback funkciju reject
u slučaju da ne pronađe poruke (to jest, ne bi trebalo da "prijavljuje grešku"), već bi samo trebalo da vrati prazan spisak poruka, unutar metode resolve
.
(Greška bi mogla biti prijavljena samo ukoliko je prosleđen pogrešan id, naziv tabele, ili, neki drugi podatak koji bi se koristio u konkretnoj implementaciji.)
Ovako definisan promis omogućava da ne menjamo pristup (u naknadnoj obradi), onda kada znamo da korisnik postoji, ali ne znamo da li ima, ili nema poruka.
(Ako nema poruka, spisak će jednostavno biti prazan.)
Pozivanje promisa
Promis koji je smešten unutar funkcije, poziva se navođenjem imena funkcije, posle čega - budući da funkcija vraća objekat klase Promise
- sledi nadovezivanje metoda then
i catch
.
Preko metode then
, pokreće se callback funkcija resolve
unutar promisa, a preko metode catch
, pokreće se callback funkcija reject
.
Pored metoda then
i catch
moguće je koristiti i metodu finally
, * koja sadrži delove koda koji se izvršavaju bezuslovno (tj. izvršavaju se bez obzira na to da li je dotadašnja obrada prošla bez grešaka ili nije).
Budući da funkcije resolve
i reject
nisu prave funkcije, već interfejsi, metodama then
i catch
se kao argumenti predaju imena konkretnih funkcija koje će obrađivati rezultat uspešnog ili neuspešnog pozivanja promisa, odnosno, kao u primerima koje ćemo prikazati, mogu se predati i odgovarajuće lambda funkcije (naravno, isto važi i za metodu finally
).
Dakle, u slučaju uspešne obrade, poziva se lambda funkcija unutar koje se rezultat izvršavanja promisa ispisuje u konzoli, preko funkcije console.log
, dok se u slučaju neuspešne obrade, poziva lambda funkcija preko koje se rezultat izvršavanja promisa (takođe) ispisuje u konzoli, ali (ovoga puta), preko funkcije console.error
.
Metodu finally
donekle izdvajamo, jer je u pitanju sintaksa koju nećemo često koristiti u nastavku članka, * ali, implementacija se obavlja po istom principu kao i u slučaju metoda then
i catch
.
Kao što smo ranije nagovestili, rezultat izvršavanja promisa može se takođe proslediti drugim promisima, preko metode then
, i upravo će ugnežđavanje promisa biti tema narednog odeljka.
Ugnežđavanje promisa (sa primerom)
Princip ugnežđavanja promisa najlakše se može shvatiti preko 'šeme' koja je prikazana na sledećoj slici:
Naizgled nema ugnežđavanja (tj. 'bežanja koda u desnu stranu'), i sve deluje kao obično nadovezivanje sinhronih instrukcija, ali ....
- promis
p2
se izvršava tek pošto se promisp1
uspešno izvrši; promisp3
izvršava se tek kada promisp2
vrati povoljan rezultat (a isti princip * važi i ako postoje promisip4
,p5
....pn
) - poslednja metoda
then
odgovara poslednjem promisu (međutim ....) - metoda
catch
, iako deluje kao da je takođe vezana za poslednji promis, zapravo je univerzalna i biće joj prosleđena poruka prvog promisa koji vrati grešku
Kao što vidimo (i kao što smo nagovestili), vezivanje (tj. ugnežđavanje) promisa, znatno je pregledniji pristup u odnosu na ugnežđene povratne pozive.
Primer ugnežđavanja promisa
Da bismo sve navedeno bolje razumeli, razmotrićemo konkretan primer: kreiraćemo (fiktivnu) metodu za sastavljanje polica, pri čemu operacija sastavljanja prolazi kroz tri faze:
- naručivanje delova
- provera delova
- sastavljanje police
U svemu, pokretanje sledeće etape, zavisi (naravno), od uspešnog okončanja prethodne etape.
Za početak, definisaćemo nekoliko kontrolnih promenljivih preko kojih se lako može upravljati tokom programa ....
Nakon prethodnog koda, definisaćemo i prvi promis, preko koga se proverava da li su naručeni delovi pristigli:
Namera nam je (zapravo), da sve promise (uključujući i prvi), definišemo po istom obrascu: funkciji se predaje objekat sa tri polja (uslov
, poruka_ok
i poruka_err
); u slučaju uspešne obrade, funkcija vraća promis koji takođe sadrži objekat sa tri polja (naravno, sa novim sadržajem), dok, u slučaju da je došlo do greške, funkcija vraća poruku o grešci.
Polja objekta imaju sledeće namene:
- polje
uslov
određuje da li će data etapa biti završena (u pitanju je samo 'simulacija', ali, i dalje važi dogovor da ćemo po nekim pitanjima "žmuriti na jedno oko") - polje
poruka_ok
predstavlja tekstualnu poruku koju je prosledio prethodni promis, i takva poruka će biti ispisana u konzoli - u slučaju da je obrada prošla na očekivani način. - polje
code_err
takođe sadrži poruku koju je prosledio prethodni promis - koja će biti ispisana (ovoga puta), u slučaju pojave greške
Za pozivanje prvog promisa kreiraćemo pomoćni objekat po prethodno navedenom obrascu, i sada se "sastavljanje police" može pokrenuti preko sledećeg koda (u nastavku ćemo dodati i ostale delove):
Da bismo dovršili proveru (svih uslova), definisaćemo i ostale promise, koji takođe funkcionišu po istom obrascu kao prvi promis:
- u slučaju uspešne obrade, ispisuje se poruka koju je prosledio prethodni promis i potom se sledećem promisu prosleđuju paket u formatu
uslov
+poruka_ok
+poruka_err
- u slučaju greške, ispisuje se poruka o grešci
Definisaćemo odmah i oba preostala promisa.
- Promis za proveru naručenih delova:
- Promis koji proverava da li je procedura za sklapanje delova protekla na predviđeni način:
Sada možemo napisati i programski kod preko koga se (svi) promisi pozivaju, pri čemu (ponovo) vidimo kako uzastopno navođenje metoda then
predstavlja znatno pregledniju zamenu za ugnežđene pozive:
Primećujemo da pretposlednji promis ne predaje kao rezultat objekat, već, običnu tekstualnu poruku, i stoga je i lambda funkcija u poslednjoj metodi then
prilagođena drugačijem formatu podataka.
Metoda catch
(da ponovimo), reaguje na prvu grešku koja se pojavi u lancu promisa:
- ako funkcija
narucivanjePromise
vrati grešku, preko naredbi iz odeljkacatch
ispisuje se poruka: "Greška: Naručeni delovi NISU DOSTAVLJENI" - ako funkcija
proveraDelovaPromise
vrati grešku, preko naredbi iz odeljkacatch
ispisuje se poruka: "Greška: Delovi NISU PROŠLI PROVERU!" - ako funkcija
sastavljanjePromise
vrati grešku, preko naredbi iz odeljkacatch
ispisuje se poruka: "Greška: Nešto je krenulo naopako pri sastavljanju police!"
Da bismo kompletirali primer, ukomponovaćemo prethodni poziv u funkciju preko koje se (detaljno) može pratiti pozivanje promisa:
Primećujemo da se skripta izvršava asinhrono (probna poruka #2 pojavljuje se gotovo odmah na početku), a primećujemo i to da je izvršavanje promisa međusobno uslovljeno - baš kao što smo i očekivali.
Pre nego što pređemo na async/await
sintaksu, osvrnućemo se na pojednostavljene načine za kreiranje promisa i (što je važnije), mehanizam za pokretanje više promisa odjednom.
Pojednostavljeni načini za kreiranje promisa
Način za kreiranje promisa koji smo do sada koristili, može se smatrati zvaničnim, i takav pristup koristićemo i dalje - u okolnostima kada kreiramo promise za iole ozbiljnije namene.
Međutim, klasa Promise
nudi i pojednostavljene načine za kreiranje promisa, koji se mogu koristiti kada (tokom razvoja softvera), nastane potreba za time da se "na brzaka" u program ubaci jednostavan promis (za koji, recimo, ne znamo da li će uopšte biti potreban u daljem radu).
'Brzinska inicijalizacija promisa' može se izvesti na sledeći način:
.... ili, još jednostavnije:
Naravno (kao što smo već rekli), to su samo priručne metode, koje (pogotovo poslednju), ne treba koristiti za promise koji će predstavljati iole važnije delove programa koje kreiramo.
Pokretanje više promisa odjednom - Promise.all
U dosadašnjim primerima, pokretanje više promisa podrazumevalo je uslovljeno i sekvencijalno (tj. uzastopno) pokretanje promisa jednih za drugim.
Za pokretanje više promisa istovremeno, moguće je koristiti metodu Promise.all
, kojoj se kao argument predaje lista promisa:
Rezultat izvršavanja operacije u konzoli, biće:
Sve što smo do sada videli u vezi sa promisima, govori u prilog tome da je način zapisa preko sintakse promise/then/catch
znatno pregledniji od nanizanih povratnih poziva sa kojima smo se na početku sreli ("callback hell"), ali, recimo da se težilo tome da se korisnicima omogući da asinhrone pozive ostvarene preko promisa, zapišu na način koji je još koncizniji, i koji, po svojim spoljnim odlikama, skoro potpuno podseća na standardni, sinhroni JS.
Sa revizijom JS-a ES7 (iz 2017. godine), na scenu je stupila async/await
sintaksa ....
Sintaksa async/await - način za urednije pozivanje promisa
Funkcije sa prefiksom async
(tj. 'asinhrone funkcije'), neposredno operišu nad promisima i imaju sledeće odlike:
- funkcija sa prefiksom
async
obavezno vraća objekat klasePromise
* - pojava rezervisane reči
await
(unutar tela funkcije), obavezuje funkciju da sačeka da promis bude rešen ** - rezervisana reč
await
može se koristiti samo unutar funkcija sa prefiksomasync
S obzirom na to da je async/await
sintaksa prilično intuitivna, razmotrićemo odmah primer (u kome ćemo koristiti promise koje smo kreirali u prethodnom poglavlju) ....
Prethodno definisani promis (koji se sada vezuje za referencu narucivanje
), postavićemo unutar funkcije, kreiraćemo i pomoćni objekat podaci
, a dodaćemo i konzolni ispis (da biste lakše mogli da samostalno isprobate skriptu):
U gornjem primeru (kao i inače), preko rezervisane reči await
daje se nalog skripti da sačeka izvršavanje promisa, pre daljeg korišćenja rezultata izvršavanja pokrenutog promisa.
Povoljan rezultat izvršavanja je rešen promis (figurativno: "ispunjeno obećanje"), i takav promis se nadalje može iskoristiti kao ulazna vrednost za pozivanje sledeće async
funkcije.
Na prethodno opisani način, sada se lako mogu povezati svi promisi koje smo ranije kreirali:
Sintaksa zaista deluje pregledno, ali, postoji nešto na šta se nismo osvrnuli: šta se događa ukoliko neki od promisa nije ispunjen?
Naizgled, neće se desiti mnogo i skripta će preko konzole prikazati da je došlo do greške.
Međutim, nije baš previše elegantno da ostavimo da se greške u konzoli pojavljuju "na silu" (zapravo - nije ni najmanje elegantno :)), i stoga ćemo greške 'pohvatati' preko bloka try-catch
.
Naredbe koje smo videli na prethodnoj slici, potrebno je smestiti u blok try
, a naredbe koje se tiču obrade grešaka, potrebno je smestiti u odeljak catch
....
.... čime smo postigli istu funkcionalnost koju je imala funkcija pokretanjePromise
(iz prethodnog poglavlja), a kada poslednji blok koda uporedimo sa ugnežđenom funkcijom iz odeljka o povratnim pozivima, ostaje nam samo da 'klimnemo glavom u znak odobravanja' (jer, iako i async/await
sintaksa u praksi može biti "ne baš skroz jednostavna" (u određenim komplikovanijim primerima), jasno je ipak da smo znatno unapredili preglednost koda u odnosu na "callback hell" sintaksu od koje smo krenuli).
Nakon svega, dođosmo (konačno) i do web workera ....
Web worker(i)
Ako ste kojim slučajem zaboravili čemu služe web workeri (tokom poduže priče o ostalim asinhronim zahtevima), podsetićemo se na to da je u pitanju tehnika za paralelno pokretanje više nezavisnih skripti na računaru klijenta.
Skripta iz primera koji ste videli na početku ....
.... zablokiraće stranicu na (cca.) nekoliko desetina sekundi, što svakako kvari iskustvo posetiocima sajta.
Web workeri rešavaju navedeni problem, uvođenjem zasebne skripte koja će biti pokrenuta na zasebnom thread-u na računaru klijenta (što znači, bar u najboljem slučaju, da će skripta praktično biti pokrenuta i na zasebnom procesorskom jezgru).
Preko worker skripte, glavni thread se oslobađa određenog zadatka koji je procesorski zahtevan (to jest, glavni thread je odblokiran).
Da bismo web workere isprobali u praksi, kreirajte HTML datoteku i unesite sledeći sadržaj u <body>
element:
.... a potom unesite sledeći kod u skriptu skripta.js
:
Očigledno je da programski kod u datoteci skripta.js
više ne sadrži petlju koju smo videli u "sinhronoj" verziji skripte.
Umesto toga, preko objekta klase Worker
, radna skripta, skripta.js
, može se povezati sa skriptom worker.js
....
.... i može pokrenuti worker skriptu preko komande postMessage
.
Svaki put kada Worker
vrati poruku, sadržaj elementa sa id-om poruka1
biće osvežen (pri čemu zapažamo da je skripta worker.js
napisana tako da šalje poruku ('nazad'), na svakih (cca.) 1% obavljenog zadatka).
Da biste odmah mogli da isprobate sve što smo naveli, možete ispratiti sledeći link:
Klikom na prvo dugme, pokreće se asinhrona skripta, tj. web worker koji obavlja zadatak nezavisno od glavnog thread-a, i sada - dok traje obrada - moguće je:
- kliknuti na drugo dugme
- birati tekst u donjem pasusu
.... a omogućen je i neometan prikaz GIF animacije.
Za kraj ....
Pored toga što nismo hteli da previše zalazimo u tematiku event loop-a, takođe nismo ovoga puta hteli ni da 'širimo priču' o porukama koje se mogu razmenjivati između Web workera i osnovne skripte (a takve poruke svakako mogu biti 'zanimljivije' od običnog teksta).
Ali, "ima dana za megdana", i stoga ćemo navedenim temama (i drugim temama), posvetiti više pažnje u narednim člancima.
Objavićemo uskoro i mali tutorijal sa dodatnim uputstvima za korišćenje promisa (sa ili bez async/await sintakse, uz dodatne primere), a sledeći članak biće posvećen Fetch API zahtevima ....