Introduzione
Per molti programmi in JavaScript, il codice viene eseguito mentre lo sviluppatore lo scrive, riga per riga. Questa è chiamata esecuzione sincrona (synchronous execution), perché le righe vengono eseguite una dopo l'altra, nell'ordine in cui sono state scritte. Tuttavia, non tutte le istruzioni fornite al computer devono essere seguite immediatamente. Ad esempio, se si invia una richiesta di rete, il processo che esegue il codice dovrà attendere la restituzione dei dati prima di poter funzionare su di esso. In questo caso, sarebbe sprecato tempo se non eseguisse altro codice in attesa del completamento della richiesta di rete. Per risolvere questo problema, gli sviluppatori utilizzano la programmazione asincrona (asynchronous programming), in cui le righe di codice vengono eseguite in un ordine diverso da quello in cui sono state scritte. Con la programmazione asincrona, possiamo eseguire altro codice mentre aspettiamo che lunghe attività come le richieste di rete finiscano.
Il codice JavaScript viene eseguito su un singolo thread all'interno di un processo del computer. Il suo codice viene elaborato in modo sincrono su questo thread, con una sola istruzione eseguita alla volta. Pertanto, se dovessimo eseguire un'attività di lunga durata su questo thread, tutto il codice rimanente viene bloccato fino al completamento dell'attività. Sfruttando le funzionalità di programmazione asincrona di JavaScript, possiamo scaricare le attività a esecuzione prolungata su un thread in background per evitare questo problema. Quando l'attività è completa, il codice di cui abbiamo bisogno per elaborare i dati dell'attività viene reinserito nel thread singolo principale.
Prerequisiti
- Node.js installato sulla macchina di sviluppo.
L'Event Loop
Cominciamo studiando il funzionamento interno dell'esecuzione della funzione JavaScript. Capire come si comporta questo ti consentirà di scrivere codice asincrono in modo più deliberato e ti aiuterà con la risoluzione dei problemi del codice in futuro.
Quando l'interprete JavaScript esegue il codice, ogni funzione chiamata viene aggiunta allo stack di chiamate di JavaScript . Lo stack di chiamate è uno stack: una struttura di dati simile a un elenco in cui gli elementi possono essere aggiunti solo all'inizio e rimossi dall'alto. Gli stack seguono il principio "Last in, first out" o LIFO. Se aggiungi due elementi alla pila, l'elemento aggiunto più di recente viene rimosso per primo.
Illustriamo con un esempio utilizzando lo stack di chiamate. Se JavaScript rileva una funzione chiamata functionA()
, viene aggiunta allo stack di chiamate. Se quella funzione functionA()
chiama un'altra funzione functionB()
, functionB()
viene aggiunta in cima allo stack di chiamate. Quando JavaScript completa l'esecuzione di una funzione, viene rimosso dallo stack di chiamate. Pertanto, JavaScript eseguirà prima functionB()
, lo rimuoverà dallo stack al termine, quindi terminerà l'esecuzione functionA()
e lo rimuoverà dallo stack di chiamate. Questo è il motivo per cui le funzioni interne vengono sempre eseguite prima delle loro funzioni esterne.
Quando JavaScript incontra un'operazione asincrona, come la scrittura su un file, la aggiunge a una tabella nella sua memoria. Questa tabella memorizza l'operazione, la condizione per il suo completamento e la funzione da chiamare quando è completata. Al termine dell'operazione, JavaScript aggiunge la funzione associata alla coda dei messaggi . Una coda è un'altra struttura di dati simile a un elenco in cui gli elementi possono essere aggiunti solo in fondo ma rimossi dall'alto. Nella coda dei messaggi, se due o più operazioni asincrone sono pronte per l'esecuzione delle relative funzioni, l'operazione asincrona completata per prima avrà la sua funzione contrassegnata per l'esecuzione per prima.
Le funzioni nella coda dei messaggi sono in attesa di essere aggiunte allo stack di chiamate. Il ciclo di eventi è un processo perpetuo che controlla se lo stack di chiamate è vuoto. In tal caso, il primo elemento nella coda dei messaggi viene spostato nello stack di chiamate. JavaScript assegna la priorità alle funzioni nella coda dei messaggi rispetto alle chiamate di funzione che interpreta nel codice. L'effetto combinato dello stack di chiamate, della coda dei messaggi e del ciclo di eventi consente l'elaborazione del codice JavaScript durante la gestione delle attività asincrone.
Ora che hai una conoscenza di alto livello del ciclo di eventi, sai come verrà eseguito il codice asincrono che scrivi. Con queste conoscenze, ora puoi creare codice asincrono con tre diversi approcci: callback, promesse e async
/await
.
Programmazione asincrona con callback
Una funzione di callback è quella che viene passata come argomento a un'altra funzione e quindi eseguita quando l'altra funzione è terminata. Utilizziamo i callback per garantire che il codice venga eseguito solo dopo il completamento di un'operazione asincrona.
Per molto tempo, i callback sono stati il meccanismo più comune per scrivere codice asincrono, ma ora sono diventati in gran parte obsoleti perché possono rendere il codice poco leggibile. In questo passaggio, scriverai un esempio di codice asincrono utilizzando i callback in modo da poterlo utilizzare come base per vedere la maggiore efficienza di altre strategie.
Esistono molti modi per utilizzare le funzioni di callback in un'altra funzione. Generalmente, prendono questa struttura:
function asynchronousFunction([ Function Arguments ], [ Callback Function ]) {
[ Action ]
}
Sebbene non sia sintatticamente richiesto da JavaScript o Node.js avere la funzione di callback come ultimo argomento della funzione esterna, è una pratica comune che rende i callback più facili da identificare. È anche comune per gli sviluppatori JavaScript utilizzare una funzione anonima come callback. Le funzioni anonime sono quelle create senza un nome. Di solito è molto più leggibile quando una funzione è definita alla fine dell'elenco degli argomenti.
Per dimostrare i callback, creiamo un modulo Node.js che scrive un elenco di film di Studio Ghibli su un file. Innanzitutto, crea una cartella che memorizzerà il nostro file JavaScript e il suo output con il comando mkdir:
mkdir ghibliMovies
Quindi accedi alla cartella con il comando cd:
cd ghibliMovies
Inizieremo effettuando una richiesta HTTP all'API di Studio Ghibli, di cui la nostra funzione di callback registrerà i risultati. Per fare ciò, installeremo una libreria che ci consente di accedere ai dati di una risposta HTTP in una richiamata.
Nel tuo terminale, inizializza npm in modo da poter avere un riferimento per i nostri pacchetti in seguito:
npm init -y
Quindi, installa la libreria request
:
npm i request --save
Ora apri un nuovo file chiamato callbackMovies.js
in un editor di testo come nano
:
nano callbackMovies.js
Nel tuo editor di testo, inserisci il seguente codice. Cominciamo inviando una richiesta HTTP con il modulo request
:
Nella prima riga, carichiamo il modulo request
che è stato installato tramite npm. Il modulo restituisce una funzione che può effettuare richieste HTTP; quindi salviamo quella funzione nella costante request
.
Quindi effettuiamo la richiesta HTTP utilizzando la funzione request()
. Stampiamo ora i dati dalla richiesta HTTP alla console aggiungendo le modifiche evidenziate:
Quando usiamo la chiamata request()
, le diamo due parametri:
- L'URL del sito Web che stiamo cercando di richiedere
- Una funzione di callback che gestisce eventuali errori o risposte riuscite dopo che la richiesta è stata completata
La nostra funzione di callback ha tre argomenti: error
, response
, e body
. Quando la richiesta HTTP è completa, agli argomenti vengono assegnati automaticamente valori a seconda del risultato. Se l'invio della richiesta non è riuscito, conterrebbe un oggetto error
, ma response
e body
sarebbero null
. Se ha effettuato correttamente la richiesta, la risposta HTTP viene archiviata in response
. Se la nostra risposta HTTP restituisce dati (in questo esempio otteniamo JSON), i dati vengono impostati in body
.
La nostra funzione di callback verifica prima se abbiamo ricevuto un errore. È consigliabile verificare prima la presenza di errori in una richiamata in modo che l'esecuzione della richiamata non continui con dati mancanti. In questo caso, registriamo l'errore e l'esecuzione della funzione. Quindi controlliamo il codice di stato della risposta. Il nostro server potrebbe non essere sempre disponibile e le API possono cambiare facendo sì che le richieste una volta sensate diventino errate. Controllando che il codice di stato sia 200
, il che significa che la richiesta era "OK", possiamo avere la certezza che la nostra risposta sia ciò che ci aspettiamo che sia.
Infine, analizziamo il corpo della risposta a un Array
e ripetiamo ogni film per registrarne il nome e l'anno di uscita.
Dopo aver salvato e chiuso il file, esegui questo script con:
node callbackMovies.js
Otterrai il seguente output:
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014
Abbiamo ricevuto con successo un elenco di film di Studio Ghibli con l'anno in cui sono stati rilasciati. Ora completiamo questo programma scrivendo l'elenco dei film che stiamo attualmente registrando in un file.
Aggiorna il file callbackMovies.js
nel tuo editor di testo per includere il seguente codice evidenziato, che crea un file CSV con i nostri dati del film:
Notando le modifiche evidenziate, vediamo che importiamo il modulo fs
. Questo modulo è standard in tutte le installazioni di Node.js e contiene un metodo writeFile()
che può scrivere in modo asincrono su un file.
Invece di registrare i dati nella console, ora li aggiungiamo a una variabile stringa movieList
. Quindi usiamo writeFile()
per salvare il contenuto di movieList
in un nuovo file — callbackMovies.csv
. Infine, forniamo un callback per la funzione writeFile()
, che ha un argomento: error
. Questo ci consente di gestire i casi in cui non siamo in grado di scrivere su un file, ad esempio quando l'utente su cui stiamo eseguendo il processo node
non dispone di tali autorizzazioni.
Salva il file ed esegui di nuovo questo programma Node.js con:
node callbackMovies.js
Nella tua cartella ghibliMovies
, vedrai callbackMovies.csv
, che ha il seguente contenuto:
È importante notare che scriviamo nel nostro file CSV nella richiamata della richiesta HTTP. Una volta che il codice è nella funzione di callback, scriverà nel file solo dopo che la richiesta HTTP è stata completata. Se volessimo comunicare con un database dopo aver scritto il nostro file CSV, creeremmo un'altra funzione asincrona che verrebbe chiamata nel callback di writeFile()
. Più codice asincrono abbiamo, più funzioni di callback devono essere annidate.
Immaginiamo di voler eseguire cinque operazioni asincrone, ognuna in grado di essere eseguita solo quando un'altra è completa. Se dovessimo codificarlo, avremmo qualcosa del genere:
doSomething1(() => {
doSomething2(() => {
doSomething3(() => {
doSomething4(() => {
doSomething5(() => {
// final action
});
});
});
});
});
Quando i callback annidati hanno molte righe di codice da eseguire, diventano sostanzialmente più complessi e illeggibili. Man mano che il tuo progetto JavaScript cresce in dimensioni e complessità, questo effetto diventerà più pronunciato, fino a quando non sarà finalmente ingestibile. Per questo motivo, gli sviluppatori non utilizzano più i callback per gestire le operazioni asincrone. Per migliorare la sintassi del nostro codice asincrono, possiamo invece utilizzare le promesse.
Utilizzo delle promesse per una programmazione asincrona concisa
Una promessa (promise) è un oggetto JavaScript che restituirà un valore in futuro. Le funzioni asincrone possono restituire oggetti promessi invece di valori concreti. Se otteniamo un valore in futuro, diciamo che la promessa è stata mantenuta. Se riceviamo un errore in futuro, diciamo che la promessa è stata rifiutata. In caso contrario, la promessa è ancora in fase di elaborazione in uno stato in sospeso.
Le promesse assumono generalmente la forma seguente:
promiseFunction()
.then([ Callback Function for Fulfilled Promise ])
.catch([ Callback Function for Rejected Promise ])
Come mostrato in questo modello, le promesse utilizzano anche le funzioni di richiamata. Abbiamo una funzione di callback per il metodo then()
, che viene eseguita quando una promessa viene mantenuta. Abbiamo anche una funzione di callback per il metodo catch()
per gestire eventuali errori che si verificano durante l'esecuzione della promessa.
Facciamo esperienza in prima persona con le promesse riscrivendo il nostro programma Studio Ghibli per utilizzare invece le promesse.
Axios è un client HTTP basato su promesse per JavaScript, quindi andiamo avanti e installiamolo:
npm i axios --save
Ora, con il tuo editor di testo preferito, crea un nuovo file promiseMovies.js
:
nano promiseMovies.js
Il nostro programma effettuerà una richiesta HTTP con axios
e quindi utilizzerà una versione speciale basata sulla promessa di fs
per salvare in un nuovo file CSV.
Digita questo codice in promiseMovies.js
così da poter caricare Axios e inviare una richiesta HTTP all'API del film:
Nella prima riga carichiamo il modulo axios
, memorizzando la funzione restituita in una costante chiamata axios
. Quindi utilizziamo il metodo axios.get()
per inviare una richiesta HTTP all'API.
Il metodo axios.get()
restituisce una promessa. Incateniamo quella promessa in modo da poter stampare l'elenco dei film di Ghibli sulla console:
Analizziamo cosa sta succedendo. Dopo aver effettuato una richiesta HTTP GET con axios.get()
, utilizziamo la funzione then()
, che viene eseguita solo quando la promessa viene mantenuta. In questo caso, stampiamo i film sullo schermo come abbiamo fatto nell'esempio di callback.
Per migliorare questo programma, aggiungi il codice evidenziato per scrivere i dati HTTP in un file:
Inoltre, importiamo nuovamente il modulo fs
. Nota come dopo l'importazione di fs
abbiamo .promises
. Node.js include una versione promessa della libreria fs
basata su callback , quindi la compatibilità con le versioni precedenti non viene interrotta nei progetti legacy.
La prima funzione then()
che elabora la richiesta HTTP ora chiama fs.writeFile()
invece di stampare sulla console. Poiché abbiamo importato la versione basata su promesse di fs
, la nostra funzione writeFile()
restituisce un'altra promessa. In quanto tale, aggiungiamo un'altra funzione then()
per quando la promessa writeFile()
è soddisfatta.
Una promessa può restituire una nuova promessa, permettendoci di eseguire le promesse una dopo l'altra. Questo ci apre la strada per eseguire più operazioni asincrone. Questo è chiamato concatenamento di promesse ed è analogo alla nidificazione di callback. Il secondo then()
viene chiamato solo dopo aver scritto correttamente nel file.
Per completare questo programma, concatena la promessa con una funzione catch()
come evidenziato di seguito:
Se una qualsiasi promessa non viene mantenuta nella catena delle promesse, JavaScript passa automaticamente alla funzione catch()
se è stata definita. Ecco perché abbiamo solo una clausola catch()
anche se abbiamo due operazioni asincrone.
Confermiamo che il nostro programma produce lo stesso output eseguendo:
node promiseMovies.js
Nella tua cartella ghibliMovies
vedrai il file promiseMovies.csv
contenente:
Con le promesse, possiamo scrivere codice molto più conciso rispetto all'utilizzo dei soli callback. La catena promessa di callback è un'opzione più pulita rispetto alla nidificazione di callback. Tuttavia, poiché effettuiamo più chiamate asincrone, la nostra catena di promesse diventa più lunga e più difficile da mantenere.
La verbosità di callback e promesse deriva dalla necessità di creare funzioni quando abbiamo il risultato di un'attività asincrona. Un'esperienza migliore sarebbe aspettare un risultato asincrono e inserirlo in una variabile al di fuori della funzione. In questo modo, possiamo utilizzare i risultati nelle variabili senza dover creare una funzione. Possiamo raggiungere questo obiettivo con le parole chiave async
e await
.
Scrivere JavaScript con async
/await
Le parole chiave async
/await
forniscono una sintassi alternativa quando si lavora con le promesse. Invece di avere il risultato di una promessa disponibile nel metodo then()
, il risultato viene restituito come valore come in qualsiasi altra funzione. Definiamo una funzione con la parola chiave async
per dire a JavaScript che è una funzione asincrona che restituisce una promessa. Usiamo la parola chiave await
per dire a JavaScript di restituire i risultati della promessa invece di restituire la promessa stessa quando è soddisfatta.
In generale, async
/await
ha questo aspetto:
async function() {
await [Asynchronous Action]
}
Vediamo come usare async
/ await
per migliorare il nostro programma Studio Ghibli. Usa il tuo editor di testo per creare e aprire un nuovo file asyncAwaitMovies.js
:
nano asyncAwaitMovies.js
Nel tuo file JavaScript appena aperto, iniziamo importando gli stessi moduli che abbiamo usato nel nostro esempio di promessa:
Le importazioni sono le stesse di promiseMovies.js
perché async
/await
utilizza le promesse.
Ora usiamo la parola chiave async
per creare una funzione con il nostro codice asincrono:
Creiamo una nuova funzione chiamata saveMovies()
e includiamo async
all'inizio della sua definizione. Questo è importante poiché possiamo usare la parola chiave await
solo in una funzione asincrona.
Utilizza la parola chiave await
per effettuare una richiesta HTTP che ottiene l'elenco dei film dall'API Ghibli:
Nella nostra funzione saveMovies()
, facciamo una richiesta HTTP con axios.get()
come prima. Questa volta non lo concateniamo con una funzione then()
. Invece, aggiungiamo await
prima che venga chiamato. Quando JavaScript vede await
, eseguirà solo il codice rimanente della funzione axios.get()
al termine dell'esecuzione e imposta la variabile response
. L'altro codice salva i dati del film in modo che possiamo scrivere su un file.
Scriviamo i dati del film in un file:
Usiamo anche la parola chiave await
quando scriviamo nel file con fs.writeFile()
.
Per completare questa funzione, dobbiamo rilevare gli errori che le nostre promesse possono generare. Facciamolo incapsulando il nostro codice in un blocco try
/catch
:
Poiché le promesse possono fallire, racchiudiamo il nostro codice asincrono con una clausola try
/catch
. Questo catturerà tutti gli errori che vengono generati quando la richiesta HTTP o le operazioni di scrittura del file falliscono.
Infine, chiamiamo la nostra funzione asincrona saveMovies()
in modo che venga eseguita quando eseguiamo il programma con node
A prima vista, sembra un tipico blocco di codice JavaScript sincrono. Ha meno funzioni passate, il che sembra un po 'più ordinato. Queste piccole modifiche rendono il codice asincrono con async
/await
più facile da mantenere.
Prova questa iterazione del nostro programma inserendola nel tuo terminale:
node asyncAwaitMovies.js
Nella tua cartella ghibliMovies
verrà creato un nuovo file asyncAwaitMovies.csv
con i seguenti contenuti:
Hai ora utilizzato le funzionalità JavaScript async
/await
per gestire il codice asincrono.
Conclusione
In questo tutorial, hai appreso come JavaScript gestisce l'esecuzione di funzioni e la gestione di operazioni asincrone con il ciclo di eventi. Hai quindi scritto programmi che creavano un file CSV dopo aver effettuato una richiesta HTTP per i dati del film utilizzando varie tecniche di programmazione asincrona.