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, pomenuti 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 blokirati izvršavanje ostatka koda, što - osim ukoliko sistem ne omogućava procesiranje korisničkih zahteva na neki 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 glavna tema kojom ćemo se baviti u nastavku ....
Asinhrono izvršavanje koda u JS-u
(Opšti principi)
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"), ali, JS endžini takođe na raspolaganju imaju i tzv. "event loop" i još nekoliko srodnih objekata, koji se koriste za pokretanje i kontrolu izvršavanja 'asinhronih' instrukcija (o čemu ćemo detaljnije diskutovati u sledećem odeljku).
Pri prvom (ozbiljnijem) susretu sa 'tehnikalijama' koje se tiču izvršavanja JS instrukcija, svakako deluje (na prvi pogled) da ne ostaje mnogo 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 **
Call stack, event loop, task queue
Izvršavanje JS instrukcija obavlja se preko sledećih nekoliko objekata:

- instrukcije iz skripte dospevaju redom na pozivni stek ("call stack")
- sinhrone instrukcije (koje dospeju na pozivni stek), bivaju izvršene i potom uklonjene sa steka
- "asinhrone instrukcije" takođe dospevaju na pozivni stek, ali (zapravo) iniciraju obradu preko određenog Web API-ja (npr.
setTimeout
), što podrazumeva da se dati zadatak obrađuje "izvan" JS okruženja (vrlo često i na drugom/udaljenom računaru) - zahtevi koji su obrađeni preko nekog od web API-ja, smeštaju se u jedan od dva * reda za obrađene zahteve
- ukoliko je pozivni stek prazan, event-loop proverava sadržaj glavnog i sporednog reda ("task queue" i "microtask queue"), na kojima se nalaze handler metode koje su vezane za pokrenute web API zahteve
- ukoliko su u redovima prisutni zadaci, event loop redom prebacuje zadatke na pozivni stek (pri čemu prioritet imaju zahtevi iz reda "microtask queue")
Zarad boljeg razumevanja, spremili smo nekoliko interaktivnih primera (sa objašnjenjima ispod formulara):

Kao što smo videli, pozivni stek (eng. call stack) funkcioniše kao i u drugim okruženjima (instrukcije dospevaju na stek redom; ukoliko se pozove funkcija, na stek se smešta "stek okvir" koji odgovara funkciji; ukoliko 'funkcija pozove funkciju', na stek se smešta još jedan 'stek okvir' i sl), međutim, u kontekstu asinhronog izvršavanja instrukcija u JavaScript-u, od najvećeg značaja je objekat event-loop, koji prebacuje zadatke, iz dva reda (task queue i microtask queue) - na call stack.
Dakle, ukoliko na pozivnom steku postoji (već pokrenut) proces, dati proces se izvršava i neće biti pokrenuti drugi zadaci, međutim, pošto se zadatak izvrši, event-loop proverava sadržaj redova tast-queue i microtask-queue, i - ukoliko postoje zahtevi (tj. zadaci) koji su prisutni u navedenim redovima * - event-loop redom prebacuje zadatke (iz redova na call stack).
Zarad očuvanja preglednosti članka, trudili smo se da (ovoga puta) ne zalazimo previše u detalje (članak je i bez toga dovoljno obiman :)), ali, spomenimo ipak kako JS okruženje funkcioniše u različitim situacijama:
- zadaci dospevaju na call stack, i pri tom se proverava da li u redovima postoje handler funkcije zadataka koji su ranije inicirani (i sada su rešeni), i pri tom u red dospevaju i zadaci vezani za DOM, CSS, periodično osvežavanje prikaza sajta u browseru i sl.
- ukoliko instrukcije ne zahtevaju previše procesorskog vremena, JS okruženje funkcioniše bez zastoja (zahtevi se iniciraju i predaju spoljnim API-jima, rešeni zahtevi ulaze u red i (na kraju), kada dođe vreme, 'izlaze' iz reda i prelaze na pozivni stek)
- ukoliko neka od instrukcija, koja dospe na call stack, zahteva 'previše' vremena za obradu (recimo, drastičan primer bi mogla biti
while
ilifor
petlja koja se ponavlja više miliona puta), event loop je praktično blokiran, što znači da rešeni zahtevi koji su dospeli u red, neće dolaziti na call stack - sve dok se "procesorski zahtevni zadatak" ne reši
Poslednja stavka koju smo naveli, često predstavlja veliki problem u praksi (to je do sada već postalo jasno :)), š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" na linkovanoj stranici), vidimo da je tab u browseru 'zabagovan'.
Skripta nije posebno zanimljiva sama po sebi (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, sasvim uspešno demonstrira 'načelno sinhronu prirodu' JS okruženja.
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:
- nije moguće kliknuti na dugme #2
- nije moguće birati tekst.
- GIF animacija je zaustavljena (u nekim browserima)
Opšti princip funkcionisanja JS okruženja (koji smo prethodno opisali), može se prikazati slikovito na sledeći način (zamislićemo da 'pun krug predstavlja npr. 10ms izvršavanja instrukcija'):
- Ako se instrukcije mogu izvršavati blagovremeno ....

.... celo okruženje funkcioniše uredno i nema zastoja.
U nekom drugom "ciklusu", mogu se pojaviti i instrukcije koje s vezane za DOM, CSS, ažuriranje stranice i sl:

- Ukoliko se pokrene naredba koja je procesorski zahtevna (tj. izvršava se značajno duže od dogovorenih 10ms):

.... ostale naredbe su praktično blokirane (u trajanju znatno dužem od 10ms)!
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 poglavlju članka).
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 koji će biti korišćeni 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, da se još jednom podsetimo ....
Kako je uopšte moguće upućivati asinhrone zahteve?
Iako smo već objasnili (preko primera) kako je moguće pokretati asinhrone zahteve o kojima govorimo u članku (AJAX, Fetch API i sl), što jeste 'pomalo čudno' pri prvom susretu, budući da su JS endžini single-threaded programi, odgovor na pitanje iz naslova je jednostavan, ali, zavisi isključivo od toga kakve zahteve upućujemo.
Dakle (još jednom): asinhroni zahtevi o kojima smo do sada diskutovali, * ne podrazumevaju paralelnu obradu podataka i sl, već su (najpraktičnije) u pitanju programi koji se pokreću 'pored' JS endžina (a vrlo često se obrada zahteva obavlja i na udaljenim računarima).
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.
S obzirom na prethodno navedeno, ovoga puta ćemo 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 JS 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):
console.log("Prva poruka");
console.log("Druga poruka");
console.log("Treća poruka");
console.log("Četvrta poruka");
console.log("Peta poruka");
.... upotrebimo u svojstvu povratnih poziva funkcije setTimeout
:
console.log("Prva poruka");
setTimeout(() => console.log("Druga poruka"), 3000);
setTimeout(() => console.log("Treća poruka"), 2000);
setTimeout(() => console.log("Četvrta poruka"), 1000);
console.log("Peta poruka");
Po izvršavanju koda ....
Prva poruka
Peta poruka
Četvrta poruka
Treća poruka
Druga poruka
.... 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.
"Neuslovljeno pokretanje operacija" (kao u gornjem primeru), na izvestan način predstavlja retku pojavu u backend-u web aplikacija, jer pokretanje važnijih operacija 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 nema sadržaja, nema ni komentara) ....
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:
setTimeout(f1, INTERVAL_1);
setTimeout(f2, INTERVAL_2);
setTimeout(f3, INTERVAL_3);
setTimeout(f4, INTERVAL_4);
setTimeout(f5, INTERVAL_5);
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:
setTimeout(() => {
if (!f1()) return new Error("Funkcija f1 nije izvršena pravilno");
setTimeout(() => {
if (!f2()) return new Error("Funkcija f2 nije izvršena pravilno");
console.log("Čestitamo, zadatak je obavljen");
}, INTERVAL_2);
}, INTERVAL_1);
Ovakav kod već deluje pomalo "konfuzno", ali, i dalje prilično lako možemo 'pohvatati konce'.
Da pojasnimo: unutar prve funkcije setTimeout
:
setTimeout(() => {
// FUNKCIJA
}, INTERVAL_1);
.... pokreće se sledeći kod:
if (!f1()) return new Error("Funkcija f1 nije izvršena pravilno");
setTimeout(() => {
if (!f2()) return new Error("Funkcija f2 nije izvršena pravilno");
console.log("Čestitamo, zadatak je obavljen");
}, INTERVAL_2);
.... 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:
setTimeout(() => {
if (!f1()) return new Error("Funkcija f1 nije izvršena pravilno");
setTimeout(() => {
if (!f2()) return new Error("Funkcija f2 nije izvršena pravilno");
setTimeout(() => {
if (!f3()) return new Error("Funkcija f3 nije izvršena pravilno");
setTimeout(() => {
if (!f4()) return new Error("Funkcija f4 nije izvršena pravilno");
setTimeout(() => {
if (f5()) {
console.log("Čestitamo, uspeli ste!");
}
else {
console.error("Gre'ota! Niste imali sreće na samom kraju!")
}
}, INTERVAL_5);
}, INTERVAL_4);
}, INTERVAL_3);
}, INTERVAL_2);
}, INTERVAL_1);
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 (najčešće) 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 ES6 (koja se pojavila 2015. godine), * u sintaksu jezika JavaScript uvrštena 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 povezati sa funkcijama za obradu rezultata (i koristiti dalje 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' (eng. 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 - pošto promis vrati informaciju o rezultatu obrade (bez obzira na to da li je rezultat povoljan ili nepovoljan) - 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
ima jedan parametar (tj. argument) * - funkciju sa dva parametra 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
).
let promise = new Promise((resolve, reject) => {
resolve(povratna_vrednost)
})
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:
let probniPromise = new Promise((resolve, reject) => {
resolve("Sve je ok!");
});
.... a može vratiti i objekat:
let probniPromise = new Promise((resolve, reject) => {
let podaci = {
ime: "Petar",
prezime: "Kovačević",
email: "perakovac@gmail.com"
};
resolve(podaci);
});
.... 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:
const probniPromise = (uslov) => {
return new Promise((resolve, reject) => {
if (uslov) {
resolve("Podaci su uspešno pronađeni");
}
else {
reject("Greška pri čitanju podataka!");
}
});
}
Može delovati pomalo 'konfuzno' što je ovoga puta promis 'ugnežđen' 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 koji će biti korišćeni u budućim primerima, dodaćemo i funkciju setTimeout
, preko koje ćemo (kao i do sada), 'simulirati' vreme izvršavanje operacija:
const probniPromise = (uslov) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (uslov) {
resolve("Podaci su uspešno pronađeni");
}
else {
reject("Greška pri čitanju podataka!");
}
}, 1000);
});
}
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 (u najširem kontekstu), što promis 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 (shodno konkretnim zahtevima određenog problema).
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):
const pronalazenjeKorisnika = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
let korisnik = pretragaKorisnika(id);
if (korisnik != null) {
resolve({
ime: korisnik.ime,
prezime: korisnik.prezime,
email: korisnik.email
});
}
else {
reject(`Greška: korisnik sa id-om ${id} - NIJE PRONAĐEN!`);
}
}, 1000);
});
}
Sa druge strane, promis koji traži poruke korisnika (npr. u bazi podataka koja se koristi u određenoj chat aplikaciji), tipično ne treba da aktivira callback funkciju reject
u slučaju da ne pronađe poruke (to jest, ne bi trebalo da "prijavljuje grešku"), već bi bilo praktičnije da takav promis 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.)
const pronalazenjePorukaKorisnika = (id, tabela) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
let poruke = pretragaProuka(id, tabela);
if (korisnik != null) {
resolve({
poruke: poruke.listaPoruka;
});
}
else {
if (poruke.greska == "POGREŠAN ID") {
reject(`Greška: unet je pogrešan id korisnika!`);
}
else {
reject(`Greška: pogrešan naziv tabele!`);
}
}
}, 1000);
});
}
Ovako definisan promis omogućava da ne menjamo pristup (u naknadnoj obradi), onda kada znamo da korisnik postoji, ali nije poznato 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
).
Opštu šemu poziva promisa (sa obradom preko lambda funkcija), možemo videti na sledećoj slici:
promis(ulaz)
.then(rez => obrada_1(rez))
.catch(greska => obrada_greske(greska))
.finally(info => obrada_info(info))
U svojstvu jednostavnog konkretnog primera, prikazujemo pozivanje probnog promisa koji smo kreirali na kraju prošlog odeljka, i obradu koja podrazumeva prost ispis (preko konzolnih funkcija za ispis):
probniPromis(ulaz)
.then(poruka => console.log(poruka))
.catch(greska => console.error(greska))
.finally(info => console.log(info))
Dakle, u slučaju uspešne obrade (u gornjem primeru), 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 drugom (tj. 'narednom') promisu, preko metode then
.
U pitanju je veoma zanimljiv postupak (koji je isto tako veoma bitan za dalje praćenje članka i uspešno korišćenje promisa u programima koje ćete kasnije kreirati), i stoga je neophodno da se u nastavku detaljnije upoznamo sa pomenutim 'nadovezivanjem promisa'.
Nadovezivanje promisa (sa primerom)
Princip nadovezivanja promisa, najlakše se može shvatiti preko jednostavne 'šeme' (koja je prikazana na sledećoj slici):
p1(ulaz)
.then(rez_p1 => p2(rez_p1))
.then(rez_p2 => p3(rez_p2))
.then(rez_p3 => p4(rez_p3))
.then(poruka => {
console.log(poruka);
})
.catch(greska => {
console.error(greska);
})
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), nadovezivanje promisa je znatno pregledniji pristup u odnosu na ugnežđene povratne pozive.
Složeniji primer nadovezivanja promisa
Da bismo sve do sada navedene detalje razumeli što bolje, 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
Za početak, definisaćemo nekoliko kontrolnih promenljivih preko kojih se lako može upravljati tokom programa ....
let INTERVAL_NARUCIVANJE = 1000;
let INTERVAL_PROVERA_DELOVA = 1000;
let INTERVAL_SASTAVLJANJE = 1000;
let USLOV_NARUCIVANJE = true;
let USLOV_PROVERA_DELOVA = true;
let USLOV_SASTAVLJANJE = true;
Nakon postavljanja 'osnovnih okvira' skripte, definisaćemo i prvi promis, preko koga se proverava da li su naručeni delovi pristigli:
const narucivanjePromise = podaci => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (podaci.uslov) {
console.log(podaci.poruka_ok);
resolve({
uslov: USLOV_PROVERA_DELOVA,
poruka_ok: "Delovi su prošli proveru",
poruka_err: "Delovi NISU prošli proveru!"
});
}
else {
reject(`Greška: ${poruka_err}`);
}
}, INTERVAL_NARUCIVANJE);
});
}
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/drugačijim sadržajem), dok, u slučaju da je došlo do greške, funkcija vraća poruku o grešci.
Polja objekta (koji se prosleđuje narednom promisu), 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, ali, ovoga puta je u pitanju poruka koja će biti ispisana u slučaju pojave greške
resolve({
uslov: USLOV_PROVERA_DELOVA,
poruka_ok: "Delovi su prošli proveru",
poruka_err: "Delovi NISU prošli proveru!"
});
Za pozivanje prvog promisa kreiraćemo pomoćni objekat po prethodno navedenom obrascu, nakon čega se "sastavljanje police" može pokrenuti preko sledećeg koda (u nastavku ćemo dodati i ostale delove):
let podaciNarucivanje = {
uslov: USLOV_NARUCIVANJE,
poruka_ok: "Naručeni delovi su dostavljeni ....",
poruka_err: "Naručeni delovi NISU dostavljeni!"
};
narucivanjePromise(podaciNarucivanje)
.then()
.catch(greska => console.error(greska));
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đuje (novi) paket u formatu
uslov
+poruka_ok
+poruka_err
- u slučaju greške, ispisuje se poruka o grešci
Prikazaćemo odmah kod za oba preostala promisa.
- Promis za proveru naručenih delova:
const proveraDelovaPromise = podaci => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (podaci.uslov) {
console.log(podaci.poruka_ok);
resolve({
uslov: USLOV_SASTAVLJANJE,
poruka_ok: "Polica je sastavljena",
poruka_err: "Polica NIJE sastavljena"
});
}
else {
reject(`Greška: ${podaci.poruka_err}`);
}
}, INTERVAL_PROVERA_DELOVA);
});
}
- Promis koji proverava da li je procedura za sklapanje delova protekla na predviđeni način:
const sastavljanjePromise = podaci => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (podaci.uslov) {
console.log(podaci.poruka_ok);
let poruka = `KONAČAN ISHOD:\n`;
poruka += `Polica je uspešno sastavljena`;
poruka += ` i može se koristiti`;
resolve(poruka);
}
else {
let poruka = `GREŠKA! `
poruka += `Nešto je krenulo naopako`
poruka += ` pri sastavljanju police!`
reject(poruka);
}
}, INTERVAL_SASTAVLJANJE);
});
}
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:
narucivanjePromise(podaciNarucivanje)
.then(rezNarucivanje => proveraDelovaPromise(rezNarucivanje))
.then(rezProveraDelova => sastavljanjePromise(rezProveraDelova))
.then(porukaSastavljanje => console.log(porukaSastavljanje))
.catch(greska => {
console.error(greska);
console.error("KONAČAN ISHOD: Polica NIJE USPEŠNO SASTAVLJENA!");
})
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:
function PokretanjePromise() {
let podaciNarucivanje = {
uslov: USLOV_NARUCIVANJE,
poruka_ok: "Naručeni delovi su dostavljeni ....",
poruka_err: "Naručeni delovi NISU dostavljeni!"
};
console.log("PROMISE / THEN / CATCH:");
console.log("Probna poruka #1, koja je zapisana pre promisa");
console.log("Narudžbenica je poslata ....");
narucivanjePromise(podaciNarucivanje)
.then(rezNarucivanje => proveraDelovaPromise(rezNarucivanje))
.then(rezProveraDelova => sastavljanjePromise(rezProveraDelova))
.then(porukaSastavljanje => {
setTimeout(() => {
console.log(porukaSastavljanje);
}, 1000);
})
.catch(greska => {
console.error(greska);
console.error("KONAČAN ISHOD: Polica NIJE USPEŠNO SASTAVLJENA!");
})
let pp_2 = `Probna poruka #2,`
pp_2 += ` koja je zapisana posle promisa`
pp_2 += ` (zanima nas kada će se poruka #2,`
pp_2 += ` pojaviti u konzoli)`
console.log(pp_2);
}
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
Postupak kreiranja 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 u situacijama kada (tokom razvoja softvera) nastane potreba 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:
const promise1 = Promise.resolve("Dobar dan!");
.... ili, još jednostavnije:
const promise1 = "Dobar dan!";
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:
const p1 = 74;
const p2 = Promise.resolve({
ime: "Petar Kovačević",
godina_rođenja: 1979
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
let uslov = true;
if (uslov) {
resolve("Dobar dan!");
}
else {
reject("Dan bi mogao biti i bolji!");
}
}, 1000);
});
Promise.all([p1, p2, p3])
.then(rezultati => {
console.log(rezultati);
})
.catch(greske => {
console.error(greske);
});
Rezultat izvršavanja operacije u konzoli, biće:
Array[ 74, {ime: "Petar", godina_rođenja: 1979}, "Dobar dan" ]
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 ugnežđenih 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) ....
let narucivanje = await narucivanjePromise(podaci);
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 što lakše mogli da isprobate skriptu samostalno):
async function PokretanjeAsyncAwait() {
let podaci = {
uslov: USLOV_NARUCIVANJE,
poruka_ok: "Naručeni delovi su dostavljeni ....",
poruka_err: "Naručeni delovi NISU dostavljeni!"
};
let narucivanje = await narucivanjePromise(podaci);
console.log(narucivanje);
}
U gornjem primeru (a tako je i inače), preko rezervisane reči await
daje se nalog skripti da sačeka izvršavanje promisa, pre daljeg korišćenja rezultata.
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, lako se mogu povezati svi promisi koje smo ranije kreirali:
async function PokretanjeAsyncAwait() {
let podaci = {
uslov: USLOV_NARUCIVANJE,
poruka_ok: "Naručeni delovi su dostavljeni ....",
poruka_err: "Naručeni delovi NISU dostavljeni!"
};
let narucivanje = await narucivanjePromise(podaci);
let proveraDelova = await proveraDelovaPromise(narucivanje);
let sastavljanje = await sastavljanjePromise(proveraDelova);
console.log(sastavljanje);
}
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 pojave 'samo u konzoli' i da pri tom ne budu adekvatno obrađene (zapravo - nije ni najmanje elegantno :)), i stoga greške treba 'pohvatati' preko bloka try-catch
.
Naredbe koje smo videli na prethodnoj slici, smeštaju se u blok try
, a naredbe koje se tiču obrade grešaka, smeštaju se u odeljak catch
....
async function PokretanjeAsyncAwait() {
let podaci = {
uslov: USLOV_NARUCIVANJE,
poruka_ok: "Naručeni delovi su dostavljeni ....",
poruka_err: "Naručeni delovi NISU dostavljeni!"
};
console.log("ASYNC / AWAIT:");
console.log("Narudžbenica je poslata ....");
try {
let narucivanje = await narucivanjePromise(podaci);
let proveraDelova = await proveraDelovaPromise(narucivanje);
let sastavljanje = await sastavljanjePromise(proveraDelova);
console.log(sastavljanje);
}
catch(greska) {
console.error(greska);
console.error("KONAČAN ISHOD: Polica NIJE SASTAVLJENA i ne može se koristiti!");
}
}
.... čime se postiže ista 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 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 je prikazan na početku ....
let GRANICA = 1000000000;
let p = 0;
let s = 0;
for (let i = 1; i < GRANICA; i++) {
s += i;
p = s / i;
p = (p + i) / 2;
}
.... blokiraće stranicu na (cca.) nekoliko desetina sekundi, što svakako kvari iskustvo posetiocima sajta.
Prethodno navedeni problem rešava se uvođenjem zasebne "worker" skripte, koja će (sasvim očekivano) biti povezana sa osnovnom skriptom, ali, biće pokrenuta na zasebnom thread-u * na računaru klijenta (što znači da će 'radna' skripta najčešće biti pokrenuta i na zasebnom procesorskom jezgru).
Da bismo web workere isprobali u praksi, kreirajte HTML datoteku i unesite sledeći sadržaj u <body>
element:
<button id='dugme_pokretanje' onclick='racunanjeProseka()'>POKRETANJE</button>
<button id='dugme_poruka' onclick='poruka()'>KLIKNITE DUGME!</button>
<p id='p1'>
Skripta računa prosek brojeva od 1 do 2000000000
</p>
<p id='poruka1'>
Prosek je:
</p>
<p id='poruka2'>
</p>
<script src='skripta.js' defer></script>
.... a potom unesite sledeći kod u skriptu skripta.js
:
let BROJAC = 0;
let worker = undefined;
function poruka() {
alert("Ako vidite ovu poruku, sve je ok :)");
}
function racunanjeProseka() {
document.getElementById("poruka1").innerHTML = `Računanje proseka (obrada je u toku) ....`;
if (typeof(worker) == undefined) {
worker = new Worker('worker.js');
worker.postMessage("Počni da radiš");
}
else {
alert("Obrada je već u toku!");
}
worker.onmessage = e => {
BROJAC++;
document.getElementById("poruka1").innerHTML = `Prosek je: ${e.data}<br>(Obrađeno: ${BROJAC}% vrednosti)`;
if (BROJAC > 99) {
BROJAC = 0;
worker = undefined;
}
}
}
Očigledno je da programski kod u datoteci skripta.js
više ne sadrži petlju koju smo videli u "sinhronoj" verziji skripte.
Umesto sinhronog pristupa: osnovna skripta skripta.js
povezuje se (posredstvom objekta klase Worker
), sa radnom skriptom worker.js
....
/* ---------------------------------- */
// Sadržaj skripte worker.js:
/* ---------------------------------- */
let GRANICA = 2000000000;
let KORAK = GRANICA / 100;
onmessage = e => {
let p = 0;
let s = 0;
for (let i = 1; i < GRANICA; i++) {
s += i;
p = s / i;
p = (p + i) / 2;
if (i % KORAK == 0) {
postMessage(p);
}
}
postMessage(p);
}
.... nakon čega osnovna/"glavna" skripta (skripta.js
) može pokretati worker skriptu preko komande postMessage
....
/* ---------------------------------- */
// Deo skripte skripta.js koji se tiče
// pokretanja worker skripte:
/* ---------------------------------- */
if (typeof(worker) == undefined) {
worker = new Worker('worker.js');
worker.postMessage("Počni da radiš");
}
else {
alert("Obrada je već u toku!");
}
.... i može primati poruke od worker skripte, preko (lambda) funkcije koja je povezana sa događajem onmessage
:
/* ---------------------------------- */
// Deo skripte skripta.js koji se tiče
// obrade podataka koji su primljeni
// od worker skripte:
/* ---------------------------------- */
worker.onmessage = e => {
BROJAC++;
document.getElementById("poruka1").innerHTML = `Prosek je: ${e.data}<br>(Obrađeno: ${BROJAC}% vrednosti)`;
if (BROJAC > 99) {
BROJAC = 0;
worker = undefined;
}
}
Svaki put kada Worker
objekat 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 (i ostalih objekata iz JS okruženja), takođe nismo ovoga puta hteli 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 ....