Asincronitate, promise, async și await
Funcții Asincrone și Callback-uri
În execuția standard JavaScript, instrucțiunile sunt executate secvențial, una după alta. Adică, mai întâi se execută prima instrucțiune, apoi a doua și așa mai departe. Cu toate acestea, ce se întâmplă dacă una dintre aceste operațiuni necesită o perioadă îndelungată de timp pentru a se executa? De exemplu, dacă realizează un proces intensiv, cum ar fi o solicitare de rețea sau accesarea unei baze de date, ceea ce ar putea dura un timp nedefinit și uneori îndelungat.
În final, în timpul execuției secvențiale, toate operațiunile ulterioare vor aștepta finalizarea acestei operațiuni. Pentru a evita o astfel de situație, JavaScript permite evitarea acestui scenariu prin intermediul funcțiilor asincrone.
De exemplu, să definim o simplă funcție asincronă care simulează un proces de durată folosind apelul setTimeout() și o întârziere de 1 secundă, apoi afișează un număr aleatoriu pe consolă:
function asyncFunction() {
setTimeout(()=>{
let result = 22;
console.log("result:", result);
}, 1000);
}
asyncFunction();
console.log("End of program");
În loc de setTimeout(), aici ar putea fi o solicitare către o bază de date sau o resursă de rețea, care ar putea dura un timp îndelungat, iar rezultatul ar fi obținut după un anumit timp. Și, în consecință, numărul ar fi afișat pe consolă la finalul execuției programului:
End of program
result: 22
Aici vedem că funcția asincronă nu blochează execuția celorlalte instrucțiuni ale programului. Cu toate acestea, atunci când lucrăm cu astfel de funcții, ne putem confrunta cu o serie de probleme. Astfel, funcțiile asincrone nu returnează rezultatul calculului asincron prin cuvântul cheie return, ci îl transmit ca parametru al funcției de callback.
function asyncFunction() {
let result;
setTimeout(()=>{result = 22;}, 1000);
return result;
}
const asyncResult = asyncFunction();
console.log("result:", asyncResult) // result: undefined
Aici, funcția asincronă asyncFunction este apelată în manieră sincronă, iar în final obținem un rezultat nedefinit. Pentru că variabila asyncResult este stabilită înainte ca funcția asyncFunction să genereze rezultatul.
O altă problemă este legată de generarea erorilor prin operatorul throw:
function asyncFunction() {
let result;
setTimeout(()=>{
result = 22;
if(result < 50) {
throw new Error("Valoare incorectă");
}
}, 1000);
return result;
}
try {
const asyncResult = asyncFunction();
console.log("result:", asyncResult)
}
catch(error) {
console.error("Error:", error); // Această linie NU se execută
}
console.log("End of program");
Aici, gestionarea erorilor în blocul catch nu va funcționa, deoarece la momentul generării erorii, codul apelant a trecut deja și nu mai este nimeni să prindă eroarea.
Inițial, gestionarea rezultatelor și a erorilor în funcțiile asincrone presupunea utilizarea funcțiilor de callback, care erau transmise unei alte funcții și erau apelate mai târziu, la un anumit moment în timp. Cel mai simplu model de utilizare a callback-urilor:
function asyncFunction(callback) {
console.log("Before calling callback");
callback();
console.log("After calling callback");
}
function callbackFunc() {
console.log("Calling callback");
}
asyncFunction(callbackFunc);
Aici, funcția asyncFunction (condiționat funcție asincronă) acceptă o funcție de callback și o apelează în cod.
De exemplu, folosim un callback pentru a obține și a procesa rezultatul și eroarea unei funcții asincrone:
function handleResult(error, result){
if(error) { // dacă este transmisă o eroare
console.error(error);
}
else { // dacă funcția asincronă s-a încheiat cu succes
console.log("Result:", result);
}
}
function asyncFunction(callback) {
setTimeout(()=>{
let result = Math.floor(Math.random() * 100) + 1;
if(result < 50) {
// dacă este mai mic de 50, setăm eroarea
callback(new Error("Valoare incorectă: " + result), null);
}
else{
// în celelalte cazuri setăm rezultatul
callback(null, result);
}
}, 1000);
}
asyncFunction(handleResult);
În calitate de callback în funcția asincronă asyncFunction este transmisă funcția handleResult.
asyncFunction(handleResult);
Pentru exemplu, pentru ca numărul să reprezinte o valoare aleatorie, aici este folosită metoda Math.random().
let result = Math.floor(Math.random() * 100) + 1;
Dacă numărul generat este mai mic de 50, atunci setăm primul parametru al funcției handleResult, care reprezintă eroarea:
if(result < 50) {
// dacă este mai mic de 50, setăm eroarea
callback(new Error("Valoare incorectă: " + result), null);
}
În celelalte cazuri setăm rezultatul, iar pentru eroare transmitem null:
else{
// în celelalte cazuri setăm rezultatul
callback(null, result);
}
Output-ul consolii la o procesare reușită (când numărul generat este egal sau mai mare de 50):
Result: 70
Dacă numărul generat este mai mic de 50, atunci va fi afișată o eroare:
Error: Valoare incorectă: 35
Aceasta este schema clasică de utilizare a callback-urilor pentru procesarea rezultatului unei funcții asincrone. Cu toate acestea, are cel puțin un mare dezavantaj: utilizarea excesivă a funcțiilor de callback poate duce la crearea unei structuri de cod cunoscută printre dezvoltatorii JavaScript ca "callback hell" (iadul callback-urilor).
O astfel de structură de cod apare când un callback într-o funcție asincronă apelează o altă funcție asincronă, callback-ul acesteia, la rândul său, poate apela a treia funcție asincronă și așa mai departe. Un exemplu de astfel de structură:
asyncFunction((error, result) => {
asyncFunction2((error2, result2) => {
asyncFunction3((error3, result3) => {
asyncFunction4((error4, result4) => {
// some code
});
});
});
});
Și pentru a rezolva această problemă, începând cu standardul ES2015 în JavaScript, a fost adăugată suportul pentru promisiuni (promises), care vor fi analizate mai detaliat în continuare.