Il "callback hell" è un termine utilizzato per descrivere un anti-pattern che diventa difficile da leggere e mantenere a causa dell'utilizzo eccessivo di callback. Questo può accadere quando si utilizzano callback per gestire operazioni asincrone, come richieste HTTP o accesso ai dati di un database, e si finisce per avere una serie di callback annidate le une nelle altre. Il risultato è un codice indented (con rientri) che può essere difficile da comprendere e gestire, soprattutto quando si lavora su progetti di grandi dimensioni.
function getData(url, callback) {
// effettuare una richiesta HTTP a un'URL e passare i dati ottenuti a una callback
// ...
}
getData('http://example.com/data1', function(data1) {
getData('http://example.com/data2/' + data1, function(data2) {
getData('http://example.com/data3?data=' + data2, function(data3) {
console.log(data3);
});
});
});
In questo esempio, ci sono tre richieste HTTP annidate, con ogni richiesta che dipende dai dati ottenuti dalla precedente. Ciò può rendere difficile comprendere il flusso di controllo del codice e può diventare ancora più complesso se si aggiungono altre richieste o altre logiche all'interno delle callback.
Async/await
Async/await è una funzionalità introdotta in JavaScript con la versione ES2017 che offre un modo più semplice per gestire le operazioni asincrone. Con async/await, è possibile scrivere codice asincrono che appare sincrono e quindi è più facile da leggere e da comprendere, riducendo il rischio di creare un "callback hell".
Per utilizzare async/await, è necessario dichiarare una funzione con la parola chiave "async" e utilizzare l'operatore "await" all'interno di essa per attendere il risultato di un'operazione asincrona. Ad esempio:
async function getData() {
const response = await fetch('http://example.com/data');
const data = await response.json();
return data;
}
In questo esempio, la funzione getData
è dichiarata come "async" e utilizza fetch
per effettuare una richiesta HTTP. L'operatore await
viene utilizzato per attendere il risultato della chiamata a fetch
e del metodo json
per ottenere i dati in formato JSON. Ciò significa che la funzione getData
restituirà una promessa che si risolverà con i dati ottenuti dalla richiesta HTTP una volta che saranno disponibili.
È importante ricordarsi due cose:
- le funzioni dichiarate con "async" sempre restituiscono una promessa, anche se non utilizzano l'operatore "await"
- è possibile utilizzare "await" solo all'interno di funzioni dichiarate con "async".
Tornando all'esempio del callback hell, vediamo come gestire la stessa situazione utilizzando async/await
async function getData(url) {
// effettuare una richiesta HTTP a un'URL e restituire i dati ottenuti
// ...
}
async function main() {
const data1 = await getData('http://example.com/data1');
const data2 = await getData('http://example.com/data2/' + data1);
const data3 = await getData('http://example.com/data3?data=' + data2);
console.log(data3);
}
main();
In questo esempio, abbiamo dichiarato la funzione getData
come "async" e utilizzato l'operatore "await" all'interno di essa per attendere il risultato della richiesta HTTP. Abbiamo quindi creato una nuova funzione main
che utilizza "await" per attendere il risultato delle chiamate a getData
e ottenere i dati ottenuti. Grazie a questo approccio, il codice diventa più leggibile e più facile da comprendere rispetto all'utilizzo di callback annidate.
In questo modo, però, è necessario utilizzare tre operatori await per attendere la risoluzione di tre promesse. Possiamo, quindi, utilizzare il metodo Promise.all per attendere la risoluzione dell'insieme di promesse in modo sincrono.
async function main() {
try {
const [data1, data2, data3] = await Promise.all([
getData('http://example.com/data1'),
getData('http://example.com/data2/' + data1),
getData('http://example.com/data3?data=' + data2)
]);
console.log(data3);
} catch (error) {
console.error(error);
}
}
In questo modo, è possibile attendere la risoluzione di tutte le promesse in modo sincrono utilizzando una sola istruzione await
.
In termini di prestazioni, utilizzare Promise.all
può essere leggermente più efficiente rispetto all'utilizzo di più operatori await
, poiché tutte le promesse vengono risolte contemporaneamente anziché in sequenza. Tuttavia, la differenza di prestazioni può essere trascurabile in molti casi e dipende dal numero e dal tipo di promesse che si stanno gestendo.
Utilizzare i dati delle funzioni asincrone
Ovviamente è possibile utilizzare il risultato di una funzione async
all'interno di un flusso di codice sincrono in JavaScript. Ci sono diversi modi per farlo, a seconda del contesto in cui si vuole utilizzare il risultato della funzione.
Un modo per bindare il risultato di una funzione async
a una variabile è quello di utilizzare l'operatore await
all'interno di un blocco async function
. Ad esempio:
async function main() {
const result = await asyncFunction();
console.log(result);
}
In questo caso, il risultato della funzione asyncFunction
viene assegnato alla variabile result
, che può quindi essere utilizzata all'interno del codice sincrono successivo. È importante notare che il blocco di codice che segue l'istruzione await
verrà eseguito solo dopo che la funzione asyncFunction
avrà completato l'esecuzione.
Se vuoi utilizzare il risultato di una funzione async
in un contesto in cui non è possibile utilizzare l'operatore await
, puoi utilizzare il metodo then
della promise restituita dalla funzione async
. Ad esempio:
asyncFunction().then(result => console.log(result));
In questo modo, il codice all'interno del blocco then
verrà eseguito solo quando la funzione asyncFunction
avrà completato l'esecuzione e la promise sarà stata risolta.