JavaScript asincron: de la Callback Hell la Async și Await
Publicat: 2022-03-11Una dintre cheile pentru a scrie o aplicație web de succes este posibilitatea de a efectua zeci de apeluri AJAX pe pagină.
Aceasta este o provocare tipică de programare asincronă, iar modul în care alegeți să faceți față apelurilor asincrone, în mare parte, va face sau vă va distruge aplicația și, prin extensie, întregul dvs. pornire.
Sincronizarea sarcinilor asincrone în JavaScript a fost o problemă serioasă pentru o perioadă foarte lungă de timp.
Această provocare afectează dezvoltatorii back-end care folosesc Node.js la fel de mult ca și dezvoltatorii front-end care folosesc orice cadru JavaScript. Programarea asincronă face parte din munca noastră de zi cu zi, dar provocarea este adesea luată cu ușurință și nu este luată în considerare la momentul potrivit.
O scurtă istorie a JavaScript asincron
Prima și cea mai simplă soluție a venit sub forma unor funcții imbricate ca apeluri inverse . Această soluție a dus la ceva numit callback hell , iar prea multe aplicații încă simt arderea acesteia.
Apoi, avem Promisiuni . Acest model a făcut codul mult mai ușor de citit, dar a fost departe de principiul Don’t Repeat Yourself (DRY). Au existat încă prea multe cazuri în care a trebuit să repeți aceleași bucăți de cod pentru a gestiona corect fluxul aplicației. Cea mai recentă adăugare, sub formă de instrucțiuni async/wait, a făcut în sfârșit codul asincron în JavaScript la fel de ușor de citit și de scris ca orice altă bucată de cod.
Să aruncăm o privire la exemplele fiecăreia dintre aceste soluții și să reflectăm asupra evoluției programării asincrone în JavaScript.
Pentru a face acest lucru, vom examina o sarcină simplă care efectuează următorii pași:
- Verificați numele de utilizator și parola unui utilizator.
- Obțineți roluri de aplicație pentru utilizator.
- Înregistrați timpul de acces la aplicație pentru utilizator.
Abordarea 1: Callback Hell („Pyramid of Doom”)
Vechea soluție de sincronizare a acestor apeluri a fost prin apeluri imbricate. Aceasta a fost o abordare decentă pentru sarcini JavaScript asincrone simple, dar nu s-ar extinde din cauza unei probleme numite callback hell.
Codul pentru cele trei sarcini simple ar arăta cam așa:
const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };
Fiecare funcție primește un argument care este o altă funcție care este apelată cu un parametru care este răspunsul acțiunii anterioare.
Prea mulți oameni vor experimenta înghețarea creierului doar citind propoziția de mai sus. A avea o aplicație cu sute de blocuri de cod similare va cauza și mai multe probleme persoanei care întreține codul, chiar dacă l-a scris ea însăși.
Acest exemplu devine și mai complicat odată ce îți dai seama că o database.getRoles
de date.getRoles este o altă funcție care are apeluri imbricate.
const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };
Pe lângă faptul că are cod greu de întreținut, principiul DRY nu are absolut nicio valoare în acest caz. Gestionarea erorilor, de exemplu, se repetă în fiecare funcție și apelarea principală este apelată din fiecare funcție imbricată.
Operațiuni JavaScript asincrone mai complexe, cum ar fi trecerea în buclă prin apeluri asincrone, reprezintă o provocare și mai mare. De fapt, nu există nicio modalitate trivială de a face acest lucru cu apeluri inverse. Acesta este motivul pentru care bibliotecile JavaScript Promise precum Bluebird și Q au obținut atât de multă tracțiune. Ele oferă o modalitate de a efectua operațiuni comune asupra solicitărilor asincrone pe care limba în sine nu le oferă deja.
Aici intervin promisiunile native JavaScript.
JavaScript Promise
Promisiunile au fost următorul pas logic în scăparea iadului de apelare. Această metodă nu a eliminat utilizarea apelurilor inverse, dar a făcut înlănțuirea funcțiilor simplă și a simplificat codul, făcându-l mult mai ușor de citit.

Cu Promises în vigoare, codul din exemplul nostru JavaScript asincron ar arăta cam așa:
const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };
Pentru a obține acest tip de simplitate, toate funcțiile utilizate în exemplu ar trebui să fie Promisified . Să aruncăm o privire la modul în care metoda getRoles
ar fi actualizată pentru a returna o Promise
:
const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };
Am modificat metoda pentru a returna un Promise
, cu două apeluri inverse, iar Promise
însăși efectuează acțiuni din metodă. Acum, resolve
și reject
apelurilor inverse vor fi mapate la metodele Promise.then
și, respectiv, Promise.catch
.
Este posibil să observați că metoda getRoles
este încă predispusă la fenomenul piramidei de doom. Acest lucru se datorează modului în care sunt create metodele bazei de date, deoarece nu returnează Promise
. Dacă metodele noastre de acces la baza de date au returnat și Promise
, metoda getRoles
ar arăta astfel:
const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };
Abordarea 3: Async/Await
Piramida fatalității a fost atenuată semnificativ odată cu introducerea Promises. Cu toate acestea, a trebuit să ne bazăm în continuare pe apelurile inverse care sunt transmise la metodele .then
și .catch
ale unei Promise
.
Promisele au deschis calea către una dintre cele mai tari îmbunătățiri din JavaScript. ECMAScript 2017 a adus zahăr sintactic pe deasupra Promises în JavaScript sub formă de declarații async
și await
.
Ele ne permit să scriem cod bazat pe Promise
ca și cum ar fi sincron, dar fără a bloca firul principal, așa cum demonstrează acest exemplu de cod:
const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };
În așteptarea rezolvării Promise
este permisă numai în cadrul funcțiilor async
, ceea ce înseamnă că verifyUser
a trebuit să fie definit folosind async function
.
Cu toate acestea, odată ce această mică modificare este făcută, puteți await
orice Promise
fără modificări suplimentare în alte metode.
Async - O rezoluție mult așteptată a unei promisiuni
Funcțiile asincrone sunt următorul pas logic în evoluția programării asincrone în JavaScript. Acestea vă vor face codul mult mai curat și mai ușor de întreținut. Declararea unei funcții ca async
vă va asigura că returnează întotdeauna o Promise
, astfel încât să nu vă mai faceți griji pentru asta.
De ce ar trebui să începeți să utilizați funcția async
JavaScript astăzi?
- Codul rezultat este mult mai curat.
- Gestionarea erorilor este mult mai simplă și se bazează pe
try
/catch
la fel ca în orice alt cod sincron. - Depanarea este mult mai simplă. Setarea unui punct de întrerupere în interiorul unui bloc
.then
nu se va muta la următorul.then
deoarece trece doar prin codul sincron. Dar, puteți trece prin apelurile înawait
ca și cum ar fi apeluri sincrone.