JavaScript asincrono: da Callback Hell a Async e Await

Pubblicato: 2022-03-11

Una 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:

  1. Verifica il nome utente e la password di un utente.
  2. Ottieni i ruoli dell'applicazione per l'utente.
  3. 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.

Illustrazione: anti-pattern di callback asincrono JavaScript

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.

Illustrazione: diagramma di promesse JavaScript asincrono

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?

  1. Il codice risultante è molto più pulito.
  2. La gestione degli errori è molto più semplice e si basa su try / catch proprio come in qualsiasi altro codice sincrono.
  3. 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 in await come se fossero chiamate sincrone.