JavaScript asincrono: da Callback Hell a Async e Await
Pubblicato: 2022-03-11Una delle chiavi per scrivere un'applicazione web di successo è essere in grado di effettuare decine di chiamate AJAX per pagina.
Questa è una tipica sfida di programmazione asincrona e il modo in cui scegli di gestire le chiamate asincrone, in gran parte, creerà o interromperà la tua app e, per estensione, potenzialmente l'intero avvio.
La sincronizzazione delle attività asincrone in JavaScript è stata un problema serio per molto tempo.
Questa sfida riguarda gli sviluppatori back-end che utilizzano Node.js tanto quanto gli sviluppatori front-end che utilizzano qualsiasi framework JavaScript. La programmazione asincrona fa parte del nostro lavoro quotidiano, ma spesso la sfida viene presa alla leggera e non considerata al momento giusto.
Una breve storia di JavaScript asincrono
La prima e la più semplice soluzione è arrivata sotto forma di funzioni nidificate come callback . Questa soluzione ha portato a qualcosa chiamato callback hell e troppe applicazioni ne risentono ancora.
Poi, abbiamo Promises . Questo modello ha reso il codice molto più facile da leggere, ma era molto diverso dal principio di non ripetere te stesso (DRY). C'erano ancora troppi casi in cui dovevi ripetere gli stessi pezzi di codice per gestire correttamente il flusso dell'applicazione. L'ultima aggiunta, sotto forma di istruzioni async/await, ha finalmente reso il codice asincrono in JavaScript facile da leggere e scrivere come qualsiasi altro pezzo di codice.
Diamo un'occhiata agli esempi di ciascuna di queste soluzioni e riflettiamo sull'evoluzione della programmazione asincrona in JavaScript.
Per fare ciò, esamineremo un semplice compito che esegue i seguenti passaggi:
- Verifica il nome utente e la password di un utente.
- Ottieni i ruoli dell'applicazione per l'utente.
- Registra il tempo di accesso all'applicazione per l'utente.
Approccio 1: Callback Hell ("The Pyramid of Doom")
L'antica soluzione per sincronizzare queste chiamate era tramite i callback nidificati. Questo era un approccio decente per semplici attività JavaScript asincrone, ma non si sarebbe ridimensionato a causa di un problema chiamato callback hell.
Il codice per le tre semplici attività sarebbe simile a questo:
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); } }) } }) } }) };
Ogni funzione ottiene un argomento che è un'altra funzione che viene chiamata con un parametro che è la risposta dell'azione precedente.
Troppe persone sperimenteranno il congelamento del cervello solo leggendo la frase sopra. Avere un'applicazione con centinaia di blocchi di codice simili causerà ancora più problemi alla persona che mantiene il codice, anche se lo ha scritto lui stesso.
Questo esempio diventa ancora più complicato una volta che ti rendi conto che un database.getRoles
è un'altra funzione che ha callback nidificati.
const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };
Oltre ad avere un codice difficile da mantenere, il principio DRY non ha assolutamente alcun valore in questo caso. La gestione degli errori, ad esempio, viene ripetuta in ciascuna funzione e il callback principale viene chiamato da ciascuna funzione nidificata.
Operazioni JavaScript asincrone più complesse, come il ciclo di chiamate asincrone, rappresentano una sfida ancora più grande. In effetti, non esiste un modo banale per farlo con i callback. Questo è il motivo per cui le librerie JavaScript Promise come Bluebird e Q hanno ottenuto così tanto successo. Forniscono un modo per eseguire operazioni comuni su richieste asincrone che il linguaggio stesso non fornisce già.
È qui che entrano in gioco le promesse JavaScript native.
JavaScript promesse
Le promesse erano il passo logico successivo per sfuggire all'inferno delle richiamate. Questo metodo non ha eliminato l'uso dei callback, ma ha reso semplice il concatenamento delle funzioni e semplificato il codice, rendendolo molto più facile da leggere.

Con Promise in atto, il codice nel nostro esempio JavaScript asincrono sarebbe simile a questo:
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 }); };
Per ottenere questo tipo di semplicità, tutte le funzioni utilizzate nell'esempio dovrebbero essere Promisified . Diamo un'occhiata a come il metodo getRoles
verrebbe aggiornato per restituire una Promise
:
const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };
Abbiamo modificato il metodo per restituire un Promise
, con due callback, e il Promise
stesso esegue azioni dal metodo. Ora, i callback di resolve
e reject
verranno mappati rispettivamente sui metodi Promise.then
e Promise.catch
.
Potresti notare che il metodo getRoles
è ancora internamente incline al fenomeno della piramide del destino. Ciò è dovuto al modo in cui vengono creati i metodi del database poiché non restituiscono Promise
. Se anche i nostri metodi di accesso al database hanno restituito Promise
, il metodo getRoles
sarebbe simile al seguente:
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) }); };
Approccio 3: Asincrono/In attesa
La piramide del destino è stata notevolmente mitigata con l'introduzione di Promises. Tuttavia, dovevamo ancora fare affidamento sui callback che vengono passati ai metodi .then
e .catch
di un Promise
.
Le promesse hanno aperto la strada a uno dei miglioramenti più interessanti di JavaScript. ECMAScript 2017 ha introdotto lo zucchero sintattico oltre a Promises in JavaScript sotto forma di async
e await
statement.
Ci consentono di scrivere codice basato su Promise
come se fosse sincrono, ma senza bloccare il thread principale, come dimostra questo esempio di codice:
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 } };
L'attesa per la risoluzione della Promise
è consentita solo all'interno delle funzioni async
, il che significa che verifyUser
doveva essere definita utilizzando async function
.
Tuttavia, una volta apportata questa piccola modifica, puoi await
qualsiasi Promise
senza ulteriori modifiche in altri metodi.
Async - Una risoluzione tanto attesa di una promessa
Le funzioni asincrone sono il prossimo passo logico nell'evoluzione della programmazione asincrona in JavaScript. Renderanno il tuo codice molto più pulito e più facile da mantenere. Dichiarare una funzione come async
assicurerà che restituisca sempre una Promise
, quindi non devi più preoccupartene.
Perché dovresti iniziare a utilizzare la funzione async
JavaScript oggi?
- Il codice risultante è molto più pulito.
- La gestione degli errori è molto più semplice e si basa su
try
/catch
proprio come in qualsiasi altro codice sincrono. - Il debug è molto più semplice. L'impostazione di un punto di interruzione all'interno di un blocco
.then
non si sposterà al.then
successivo perché si limita a scorrere il codice sincrono. Tuttavia, puoi scorrere le chiamate inawait
come se fossero chiamate sincrone.