Dacă abia începi cu programarea, sunt șanse să te gândești la programe ca la un set de blocuri secvențiale de logică, în care fiecare bloc face un anumit lucru și își transmite rezultatul, astfel încât următorul bloc să poată rula și așa mai departe, și pentru În cea mai mare parte, aveți dreptate, majoritatea programelor rulează într-o manieră secvențială, acest model ne permite să construim programe care sunt simplu de scris și întreținut. Există însă cazuri de utilizare specifice în care acest model secvenţial nu ar funcţiona sau nu ar fi optim. Ca exemplu, luați în considerare o aplicație de citire de cărți. Această aplicație are câteva funcții avansate, cum ar fi găsirea tuturor aparițiilor unui cuvânt, navigarea între marcaje și similare. Acum imaginați-vă că utilizatorul citește în prezent o carte lungă și decide să caute toate aparițiile unui cuvânt comun, cum ar fi „The”. Aplicația va dura în mod normal câteva secunde pentru a găsi și indexa toate aparițiile acelui cuvânt. Într-un program secvenţial, utilizatorul nu poate interacţiona cu aplicaţia (schimba pagina sau evidenţia un text) până când operaţiunea de căutare nu este îndeplinită. Sperăm că puteți vedea că aceasta nu este o experiență optimă pentru utilizator!
Diagrama ilustrează un flux tipic de execuție al aplicației cititor de cărți. Dacă utilizatorul inițiază o operațiune de lungă durată (în acest caz căutarea tuturor aparițiilor „the” într-o carte mare), aplicația „se îngheață” pe toată durata acelei operațiuni. În acest caz, utilizatorul va continua să facă clic pe următorul buton de marcaj fără niciun rezultat până când operațiunea de căutare este terminată și toate operațiunile vor intra în vigoare imediat, oferind utilizatorului final senzația unei aplicații întârziate.
S-ar putea să fi observat că acest exemplu nu corespunde cu adevărat modelului secvenţial pe care l-am introdus mai devreme. Acest lucru se datorează faptului că operațiunile de aici sunt independente unele de altele. Utilizatorul nu trebuie să știe despre numărul de apariții ale „the” pentru a naviga la următorul marcaj, astfel încât ordinea de execuție a operațiunilor nu este cu adevărat importantă. Nu trebuie să așteptăm sfârșitul operațiunii de căutare înainte de a putea naviga la următorul marcaj. O posibilă îmbunătățire a fluxului de execuție anterior se bazează pe această logică: putem rula operația de căutare lungă în fundal, putem continua cu orice operațiuni primite și, odată ce operațiunea lungă este finalizată, putem pur și simplu să anunțăm utilizatorul. Fluxul de execuție devine după cum urmează:
Cu acest flux de execuție, experiența utilizatorului este îmbunătățită semnificativ. Acum, utilizatorul poate iniția o operațiune de lungă durată, poate continua să utilizeze aplicația în mod normal și să fie notificat odată ce operațiunea este finalizată. Aceasta este baza programării asincrone.
Javascript, printre alte limbi, acceptă acest stil de programare asincronă, oferind API-uri extinse pentru a obține aproape orice comportament asincron la care vă puteți gândi. La sfârșitul zilei, Javascript ar trebui să fie în mod inerent un limbaj asincron. Dacă ne referim la exemplul anterior, logica asincronă se află la baza tuturor aplicațiilor de interacțiune cu utilizatorul, iar Javascript a fost construit în primul rând pentru a fi utilizat în browser, unde majoritatea programelor sunt despre răspunsul la acțiunile utilizatorului.
Următoarele vă vor oferi un scurt ghid despre Javascript asincron:
Reapeluri
Într-un program tipic, veți găsi de obicei o serie de funcții. Pentru a folosi o funcție, o numim cu un set de parametri. Codul funcției se va executa și va returna un rezultat, nimic ieșit din comun. Programarea asincronă modifică ușor această logică. Revenind la exemplul aplicației cititor de cărți, nu putem folosi o funcție obișnuită pentru a implementa logica operației de căutare, deoarece operația durează o perioadă necunoscută. O funcție obișnuită va reveni practic înainte de finalizarea operației, iar acesta nu este comportamentul la care ne așteptăm. Soluția este să specificați o altă funcție care va fi executată odată ce operația de căutare este finalizată. Aceasta modelează cazul nostru de utilizare deoarece programul nostru își poate continua fluxul în mod normal și odată ce operațiunea de căutare este terminată, funcția specificată se va executa pentru a notifica utilizatorul cu privire la rezultatele căutării. Această funcție este ceea ce numim o funcție de apel invers:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
În primul rând, definim funcția de operare de căutare, searchOccurrences. Este nevoie de cuvântul de căutat și de un al doilea parametru „callback”, care va fi funcția de executat odată ce operația de căutare este finalizată. Funcția operației de căutare a fost păstrată în mod intenționat abstractă, trebuie să ne concentrăm doar asupra celor două rezultate posibile: primul caz este în care totul a mers cu succes și avem rezultatul căutării în variabila rezultat. În acest caz, trebuie doar să apelăm funcția de apel invers cu următorii parametri: primul parametru este nul, ceea ce înseamnă că nu a apărut nicio eroare, al doilea parametru este cuvântul care a fost căutat și al treilea și poate cel mai important parametru dintre cei trei., este rezultatul operației de căutare.
Al doilea caz este în care apare o eroare, acesta este și un caz în care se face execuția operației de căutare și trebuie să apelăm funcția de apel invers. Folosim un bloc try and catch pentru a intercepta orice eroare și apelăm doar funcția de apel invers cu obiectul de eroare din blocul catch.
Am definit apoi funcția de apel invers, handleSearchOccurrences, i-am păstrat logica destul de simplă. Este doar o chestiune de imprimare a unui mesaj pe consolă. Mai întâi verificăm parametrul „err” pentru a vedea dacă a apărut vreo eroare în funcția principală. În acest caz, informăm utilizatorul că operațiunea de căutare s-a încheiat cu o eroare. Dacă nu au apărut erori, imprimăm un mesaj cu rezultatul operațiunii de căutare.
În cele din urmă, numim funcția searchOccurrences cu cuvântul „the”. Funcția va rula acum normal fără a bloca programul principal și odată ce căutarea este făcută, se va executa apelul înapoi și vom primi mesajul rezultat fie cu rezultatul căutării, fie cu mesajul de eroare.
Este important să menționăm aici că avem acces doar la variabila rezultat în interiorul funcțiilor principale și callback. Daca incercam asa ceva:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
rezultatul tipăririi ar fi nedefinit deoarece programele nu așteaptă să se execute funcția searchOccurrences. Se trece la următoarea instrucțiune care este instrucțiunea print înainte ca variabila rezultat să fie atribuită în cadrul funcției principale. Ca rezultat, vom tipări variabila rezultat nealocată.
Deci, pe baza acestei logici, ar trebui să păstrăm tot codul care utilizează variabila rezultat în interiorul funcției de apel invers. S-ar putea să nu pară o problemă acum, dar s-ar putea transforma rapid într-o problemă reală. Imaginați-vă cazul în care avem un lanț de funcții asincrone care trebuie să ruleze în secvență. În logica tipică de apel invers, ați implementa ceva de genul acesta:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Rețineți că fiecare callback are un parametru de eroare și fiecare eroare trebuie tratată separat. Acest lucru face codul deja complex de mai sus și mai complex și greu de întreținut. Sperăm că Promises sunt aici pentru a rezolva problema iadului de apel invers, o vom acoperi în continuare.
Promite
Promisiunile sunt construite pe lângă apelurile înapoi și funcționează într-un mod similar. Au fost introduse ca parte a caracteristicilor ES6 pentru a rezolva câteva dintre problemele flagrante legate de apelurile inverse, cum ar fi iadul de apel invers. Promisele oferă propriile funcții care rulează la finalizarea cu succes (rezolvare) și când apar erori (respingere). Următoarele prezintă exemplul searchOccurrences implementat cu promisiuni:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Să trecem peste modificările pe care le-am aplicat:
Funcția searchOccurrences returnează o promisiune. În cadrul promisiunii, păstrăm aceeași logică: avem două funcții de rezolvare și de respingere care reprezintă apelurile noastre inverse, mai degrabă decât să avem o singură funcție de apel invers care se ocupă atât de o execuție cu succes, cât și de o execuție cu erori. Promisiunile separă cele două rezultate și oferă o sintaxă curată la apelarea funcției principale. Funcția de rezolvare este „conectată” la funcția principală folosind cuvântul cheie „atunci”. Aici specificăm doar cei doi parametri ai funcției de rezolvare și imprimăm rezultatul căutării. Un lucru similar se aplică funcției de respingere, aceasta poate fi conectată folosind cuvântul cheie „catch”. Sperăm că puteți aprecia avantajele oferite de promisiuni în ceea ce privește lizibilitatea codului și curățenia. Dacă încă dezbateți, vedeți cum putem rezolva problema callback hell prin înlănțuirea funcțiilor asincrone pentru a rula una după alta:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Așteptați
Async/Await este cea mai recentă adăugare la centura noastră de instrumente asincronă în Javascript. Introduse cu ES8, ele oferă un nou strat de abstractizare pe deasupra funcțiilor asincrone, pur și simplu „așteptând” execuția unei operații asincrone. Fluxul programului se blochează la acea instrucțiune până când un rezultat este returnat de la operația asincronă și apoi programul va continua cu următoarea instrucțiune. Dacă vă gândiți la fluxul de execuție sincronă, aveți dreptate. Am completat cercul! Async/wait încearcă să aducă simplitatea programării sincrone în lumea asincronă. Vă rugăm să rețineți că acest lucru este perceput doar în execuția și codul programului. Totul rămâne la fel sub capotă, Async/wait încă folosesc promisiuni, iar apelurile sunt elementele lor de bază.
Să trecem peste exemplul nostru și să-l implementăm folosind Async/wait:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Codul nostru nu s-a schimbat prea mult, lucrul important de observat aici este cuvântul cheie „async” înainte de declarația funcției searchOccurrences. Aceasta indică faptul că funcția este asincronă. De asemenea, observați cuvântul cheie „așteaptă” atunci când apelați funcția searchOccurrences. Acest lucru va instrui programul să aștepte execuția funcției până când rezultatul este returnat înainte ca programul să poată trece la instrucțiunea următoare, cu alte cuvinte, variabila rezultat va păstra întotdeauna valoarea returnată a funcției searchOccurrences și nu promisiunea de funcția, în acest sens, Async/Await nu are o stare în așteptare ca Promises. Odată terminată execuția, trecem la instrucțiunea print și de această dată rezultatul conține efectiv rezultatul operației de căutare. După cum era de așteptat, noul cod are același comportament ca și cum ar fi sincron.
Un alt lucru minor de reținut este că, deoarece nu mai avem funcții de apel invers, trebuie să gestionăm eroarea searchOccurrences în cadrul aceleiași funcții, deoarece nu putem pur și simplu să propagam eroarea la funcția de apel invers și să o gestionăm acolo. Aici doar tipărim un mesaj de eroare în cazul unei erori de dragul exemplului.
Învelire
În acest articol am analizat diferitele abordări utilizate pentru a implementa logica asincronă în Javascript. Am început prin a explora un exemplu concret de ce ar trebui să trecem de la stilul obișnuit de programare sincronă la modelul asincron. Am trecut apoi la apelurile inverse, care sunt principalele blocuri de bază ale JavaScript asincron. Limitările apelurilor inverse ne-au condus către diferitele alternative care au fost adăugate de-a lungul anilor pentru a depăși aceste limitări, în principal promisiuni și Async/wait. Logica asincronă poate fi găsită oriunde pe web, indiferent dacă apelați un API extern, inițiați o interogare la baza de date, scrieți în sistemul de fișiere local sau chiar așteptați intrarea utilizatorului pe un formular de conectare. Sperăm că acum vă simțiți mai încrezători să rezolvați aceste probleme scriind JavaScript asincron curat și care poate fi întreținut!
Dacă vă place acest articol, vă rugăm să consultați CLA Blog unde discutăm diverse subiecte despre cum să pătrundem în tehnologie. De asemenea, consultați canalul nostru de youtube pentru atelierele noastre gratuite anterioare și urmăriți-ne pe rețelele sociale ca să nu ratați cele viitoare!