Prilikom razvoja JavaScript aplikacija, verovatno ste se susreli sa asinhronim funkcijama, poput funkcije `fetch` u pretraživaču ili funkcije `readFile` u Node.js okruženju.
Možda ste primetili neočekivane rezultate kada ste ove funkcije koristili kao da su obične. Razlog tome je što su one asinhrone. Ovaj tekst objašnjava šta to tačno znači i kako se asinhronim funkcijama upravlja na profesionalan način.
Uvod u sinhrone funkcije
JavaScript je jednoprocesorski jezik, što znači da može izvršavati samo jedan zadatak u datom trenutku. Ako procesor naiđe na funkciju koja traje dugo, JavaScript čeka da se celokupna funkcija izvrši pre nego što nastavi sa drugim delovima programa.
Većina funkcija se u potpunosti izvršava na procesoru. Tokom izvršavanja tih funkcija, procesor je potpuno zauzet, bez obzira koliko dugo to traje. Takve funkcije se nazivaju sinhronim. Primer sinhrone funkcije dat je u nastavku:
function add(a, b) { for (let i = 0; i < 1000000; i ++) { // Ne radi ništa } return a + b; } // Pozivanje funkcije će trajati sum = add(10, 5); // Međutim, procesor ne može preći na sledeći red dok se funkcija ne izvrši console.log(sum);
Ova funkcija izvršava veliku petlju koja traje neko vreme, pre nego što vrati zbir dva ulazna argumenta.
Nakon definisanja funkcije, pozvali smo je i sačuvali njen rezultat u varijablu `sum`. Zatim smo ispisali vrednost te varijable. Iako izvršavanje funkcije `add` traje, procesor ne može da pređe na ispis `sum` dok se funkcija ne završi.
Ogromna većina funkcija će se ponašati na ovaj predvidljiv način. Međutim, pojedine funkcije su asinhrone i ponašaju se drugačije od običnih.
Uvod u asinhrone funkcije
Asinhone funkcije obavljaju veći deo svog posla izvan procesora. To znači da, iako izvršavanje funkcije može trajati, procesor je slobodan i može obavljati druge zadatke.
Evo primera takve funkcije:
fetch('https://jsonplaceholder.typicode.com/users/1');
Kako bi se poboljšala efikasnost, JavaScript dozvoljava procesoru da nastavi sa drugim zadacima koji zahtevaju procesorsku snagu, čak i pre nego što je izvršavanje asinhronih funkcija završeno.
Budući da je procesor nastavio dalje pre završetka asinhronog zadatka, rezultat funkcije neće biti odmah dostupan. On će biti u stanju čekanja. Ako bi procesor pokušao da izvrši delove programa koji zavise od rezultata koji je na čekanju, došlo bi do grešaka.
Stoga, procesor treba da izvršava samo one delove programa koji ne zavise od rezultata koji su na čekanju. Za to, savremeni JavaScript koristi koncepte obećanja (promises).
Šta je obećanje (Promise) u JavaScriptu?
U JavaScriptu, obećanje (promise) je privremena vrednost koju asinhrona funkcija vraća. Obećanja su osnova modernog asinhronog programiranja u JavaScriptu.
Kada se obećanje kreira, jedna od dve stvari se može dogoditi. Ili se obećanje „razreši“ (resolve) kada se uspešno dobije povratna vrednost asinhronog zadatka, ili se „odbije“ (reject) u slučaju greške. Ovo su događaji u životnom ciklusu obećanja. Stoga, mi možemo „prikačiti“ rukovaoce događajima (event handlers) na obećanje, koji će se pozvati kada se obećanje razreši ili odbije.
Svi delovi koda koji zahtevaju konačnu vrednost asinhronog zadatka mogu biti prikačeni na rukovaoca događaja koji se poziva kada se obećanje razreši. Svi delovi koda koji se bave greškom kod odbijenog obećanja takođe će biti vezani za odgovarajućeg rukovaoca događajem.
Evo primera gde čitamo podatke iz fajla u Node.js okruženju.
const fs = require('fs/promises'); fileReadPromise = fs.readFile('./hello.txt', 'utf-8'); fileReadPromise.then((data) => console.log(data)); fileReadPromise.catch((error) => console.log(error));
U prvom redu uvozimo modul `fs/promises`.
U drugom redu pozivamo funkciju `readFile`, prosleđujući ime fajla i kodiranje, čiji sadržaj želimo da pročitamo. Ova funkcija je asinhrona, te stoga vraća obećanje. To obećanje čuvamo u varijabli `fileReadPromise`.
U trećem redu, prikačili smo osluškivača (listener) događaja koji se poziva kada se obećanje razreši. To smo učinili pozivanjem `then` metode na objektu obećanja. Kao argument `then` metodi, prosledili smo funkciju koja će se izvršiti ukoliko i kada se obećanje razreši.
U četvrtom redu, prikačili smo osluškivača za slučaj kada se obećanje odbije. To se radi pozivanjem `catch` metode i prosleđivanjem rukovaoca događajem greške kao argumenta.
Alternativni pristup je korišćenje ključnih reči `async` i `await`. Ovaj pristup ćemo razmotriti u nastavku.
Objašnjenje `async` i `await`
Ključne reči `async` i `await` se mogu koristiti za pisanje asinhronog JavaScripta na način koji sintaksno deluje čitkije. U ovom odeljku ću objasniti kako se ove ključne reči koriste i kakav efekat imaju na vaš kod.
Ključna reč `await` se koristi za pauziranje izvršavanja funkcije, dok se čeka da se asinhrona funkcija završi. Evo primera:
const fs = require('fs/promises'); async function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // Ova linija se neće izvršiti dok podaci ne budu dostupni console.log(data); } readData()
Koristili smo ključnu reč `await` prilikom pozivanja funkcije `readFile`. Time je procesoru naloženo da čeka dok se fajl ne pročita, pre nego što se sledeća linija (console.log) može izvršiti. Ovo osigurava da kod koji zavisi od rezultata asinhronog zadatka neće biti izvršen dok rezultat ne bude dostupan.
Ako biste pokušali da pokrenete gornji kod, naišli biste na grešku. To je zato što se `await` može koristiti samo unutar `async` funkcije. Da biste funkciju proglasili asinhronom, koristite ključnu reč `async` pre deklaracije funkcije, na sledeći način:
const fs = require('fs/promises'); async function readData() { const data = await fs.readFile('./hello.txt', 'utf-8'); // Ova linija se neće izvršiti dok podaci ne budu dostupni console.log(data); } // Pozivamo funkciju da se izvrši readData() // Kod ovde će se izvršiti dok se čeka da readData funkcija završi console.log('Čekamo da se podaci učitaju')
Ako pokrenete ovaj kod, videćete da JavaScript izvršava spoljašnju `console.log` liniju dok čeka da podaci iz fajla budu dostupni. Kada postanu dostupni, izvršava se `console.log` unutar funkcije `readData`.
Rukovanje greškama prilikom korišćenja ključnih reči `async` i `await` se obično vrši korišćenjem `try…catch` blokova. Takođe je važno znati kako izvršavati petlje sa asinhronim kodom.
`async` i `await` su dostupni u modernom JavaScriptu. Tradicionalno, asinhroni kod se pisao koristeći povratne pozive (callbacks).
Uvod u povratne pozive (callbacks)
Povratni poziv (callback) je funkcija koja će biti pozvana kada rezultat bude dostupan. Sav kod koji zavisi od povratne vrednosti biće stavljen u povratni poziv. Sve van povratnog poziva ne zavisi od rezultata i stoga je slobodno za izvršenje.
Evo primera koji čita fajl u Node.js okruženju.
const fs = require("fs"); fs.readFile("./hello.txt", "utf-8", (err, data) => { // U ovom povratnom pozivu, stavljamo sav kod koji zahteva rezultat if (err) console.log(err); else console.log(data); }); // Ovde možemo obavljati sve zadatke koji ne zavise od rezultata čitanja console.log("Pozdrav iz programa");
U prvom redu, uvozimo modul `fs`. Zatim pozivamo funkciju `readFile` iz `fs` modula. Funkcija `readFile` čita tekst iz navedenog fajla. Prvi argument je ime fajla, a drugi format fajla.
Funkcija `readFile` asinhrono čita tekst iz fajlova. Da bi to uradila, uzima funkciju kao argument. Ovaj argument je funkcija povratnog poziva i biće pozvana kada podaci budu pročitani.
Prvi argument koji se prosleđuje funkciji povratnog poziva je greška. Ona će imati vrednost ako se greška dogodila prilikom pokretanja funkcije. Ukoliko nije došlo do greške, biće nedefinisana.
Drugi argument koji se prosleđuje povratnom pozivu su podaci pročitani iz fajla. Kod unutar ove funkcije pristupiće podacima. Kod van ove funkcije ne zahteva podatke iz fajla i stoga se može izvršavati dok se čekaju podaci iz fajla.
Pokretanje gornjeg koda bi dalo sledeći rezultat:
Ključne JavaScript karakteristike
Postoje određene ključne karakteristike koje utiču na način na koji asinhroni JavaScript funkcioniše. One su dobro objašnjene u videu ispod:
U nastavku sam ukratko objasnio dve važne karakteristike.
#1. Jednoprocesorski (Single-threaded)
Za razliku od drugih jezika koji dozvoljavaju programeru da koristi više niti (threads), JavaScript vam dozvoljava da koristite samo jednu nit. Nit je niz instrukcija koje logički zavise jedna od druge. Više niti omogućava programu da izvršava drugu nit kada se naiđe na operacije blokiranja.
Međutim, više niti unose složenost i otežavaju razumevanje programa koji ih koriste. Zbog toga je veća verovatnoća da će greške biti unete u kod i biće teže otkloniti greške u kodu. JavaScript je napravljen da bude jednoprocesorski, radi jednostavnosti. Kao jednoprocesorski jezik, oslanja se na događajima-vođen pristup za efikasno upravljanje blokirajućim operacijama.
#2. Događajima-vođen (Event-driven)
JavaScript je takođe događajima-vođen. To znači da se određeni događaji dešavaju tokom životnog ciklusa JavaScript programa. Kao programer, možete vezati funkcije za ove događaje, i kad god se događaj desi, vezana funkcija će biti pozvana i izvršena.
Neki događaji mogu biti rezultat dostupnosti rezultata blokirajuće operacije. U tom slučaju, vezana funkcija se poziva sa rezultatom.
Stvari koje treba uzeti u obzir prilikom pisanja asinhronog JavaScripta
U ovom poslednjem odeljku ću navesti neke stvari koje treba uzeti u obzir prilikom pisanja asinhronog JavaScripta. To uključuje podršku pretraživača, najbolje prakse i važnost.
Podrška pretraživača
Ovo je tabela koja prikazuje podršku za obećanja (promises) u različitim pretraživačima.
Izvor: caniuse.com
Ovo je tabela koja prikazuje podršku za `async`/`await` ključne reči u različitim pretraživačima.
Izvor: caniuse.com
Najbolje prakse
- Uvek koristite `async`/`await` jer pomaže da se piše čistiji kod o kome se lakše razmišlja.
- Rukujte greškama u `try/catch` blokovima.
- Koristite ključnu reč `async` samo kada je potrebno čekati rezultat funkcije.
Važnost asinhronog koda
Asinhroni kod vam omogućava da pišete efikasnije programe koji koriste samo jednu nit. To je važno zato što se JavaScript koristi za pravljenje veb lokacija koje vrše mnogo asinhronih operacija, kao što su mrežni zahtevi i čitanje ili pisanje fajlova na disk. Ova efikasnost je omogućila okruženjima za izvršavanje kao što je Node.js da steknu popularnost kao preferirano okruženje za serverske aplikacije.
Završne reči
Ovo je bio opširan tekst, ali u njemu smo uspeli da pokrijemo razliku između asinhronih i običnih sinhronih funkcija. Takođe smo razmotrili kako se koristi asinhroni kod, koristeći obećanja, ključne reči `async/await` i povratne pozive.
Pored toga, razmotrili smo ključne karakteristike JavaScripta. U poslednjem odeljku smo završili sa pokrivanjem podrške pretraživača i najboljih praksi.
Zatim pogledajte pitanja za intervju o Node.js koja se često postavljaju.