L’autore ha selezionato il Fondo di Soccorso COVID-19 per ricevere una donazione come parte del programma Scrivi per le Donazioni.
Introduzione
Nelle prime fasi di internet, i siti web consistevano spesso in dati statici in una pagina HTML. Ma ora che le applicazioni web sono diventate più interattive e dinamiche, è diventato sempre più necessario effettuare operazioni intensive come richieste di rete esterne per recuperare dati API. Per gestire queste operazioni in JavaScript, un programmatore deve utilizzare tecniche di programmazione asincrona.
Dato che JavaScript è un linguaggio di programmazione single-threaded con un modello di esecuzione sincrona che elabora un’operazione dopo l’altra, può elaborare solo un’istruzione alla volta. Tuttavia, un’azione come richiedere dati da un’API può richiedere un tempo indeterminato, a seconda delle dimensioni dei dati richiesti, della velocità della connessione di rete e di altri fattori. Se le chiamate API venissero eseguite in modo sincrono, il browser non sarebbe in grado di gestire alcun input utente, come lo scorrimento o il clic su un pulsante, fino al completamento di quell’operazione. Questo è noto come blocco.
Per prevenire il comportamento di blocco, l’ambiente del browser dispone di molte Web API a cui JavaScript può accedere in modo asincrono, il che significa che possono essere eseguite in parallelo con altre operazioni anziché in sequenza. Questo è utile perché consente all’utente di continuare a utilizzare normalmente il browser mentre le operazioni asincrone vengono elaborate.
Come sviluppatore JavaScript, è necessario sapere come lavorare con le API Web asincrone e gestire la risposta o l’errore di tali operazioni. In questo articolo, imparerai riguardo al ciclo degli eventi, al modo originale di gestire il comportamento asincrono attraverso i callback, all’aggiunta aggiornata di promesse di ECMAScript 2015 e alla pratica moderna dell’utilizzo di async/await
.
Nota: Questo articolo è focalizzato su JavaScript lato client nell’ambiente del browser. Gli stessi concetti sono generalmente validi anche nell’ambiente di Node.js, tuttavia Node.js utilizza le proprie API in C++ invece delle API Web del browser.
Il Ciclo degli Eventi
Questa sezione spiegherà come JavaScript gestisce il codice asincrono con il ciclo degli eventi. Innanzitutto, verrà eseguita una dimostrazione del ciclo degli eventi in azione, e successivamente verranno spiegate le due componenti del ciclo degli eventi: lo stack e la coda.
Il codice JavaScript che non utilizza alcuna API Web asincrona verrà eseguito in modo sincrono, uno alla volta, in sequenza. Questo è dimostrato dal seguente codice di esempio che chiama tre funzioni ognuna delle quali stampa un numero sulla console:
In questo codice, si definiscono tre funzioni che stampano numeri con console.log()
.
Successivamente, si eseguono le chiamate alle funzioni:
Il risultato sarà basato sull’ordine in cui sono state chiamate le funzioni: first()
, second()
, poi third()
:
Output1
2
3
Quando viene utilizzata un’API Web asincrona, le regole diventano più complicate. Un’API integrata con cui puoi testare questo è setTimeout
, che imposta un timer e esegue un’azione dopo un certo periodo di tempo. setTimeout
deve essere asincrono, altrimenti l’intero browser rimarrebbe bloccato durante l’attesa, il che comporterebbe una scarsa esperienza utente.
Aggiungi setTimeout
alla funzione second
per simulare una richiesta asincrona:
setTimeout
prende due argomenti: la funzione che verrà eseguita in modo asincrono e la quantità di tempo che deve attendere prima di chiamare quella funzione. In questo codice hai avvolto console.log
in una funzione anonima e l’hai passata a setTimeout
, quindi hai impostato la funzione per essere eseguita dopo 0
millisecondi.
Ora chiama le funzioni, come hai fatto prima:
Potresti aspettarti che con un setTimeout
impostato su 0
millisecondi, l’esecuzione di queste tre funzioni comporti comunque la stampa dei numeri in ordine sequenziale. Ma poiché è asincrono, la funzione con il timeout verrà stampata per ultima:
Output1
3
2
Che tu imposti il timeout a zero secondi o a cinque minuti non farà differenza: il console.log
chiamato dal codice asincrono verrà eseguito dopo le funzioni sincrone di livello superiore. Questo avviene perché l’ambiente host JavaScript, in questo caso il browser, utilizza un concetto chiamato ciclo degli eventi per gestire la concorrenza o gli eventi paralleli. Poiché JavaScript può eseguire solo un’istruzione alla volta, ha bisogno che il ciclo degli eventi sia informato su quando eseguire quale specifica istruzione. Il ciclo degli eventi gestisce ciò con i concetti di pila e coda.
Pila
Lo stack, o pila di chiamate, contiene lo stato di quale funzione è attualmente in esecuzione. Se non sei familiare con il concetto di stack, puoi immaginarlo come un array con proprietà “Ultimo in, primo out” (LIFO), il che significa che puoi solo aggiungere o rimuovere elementi dalla fine dello stack. JavaScript eseguirà il frame corrente (o la chiamata di funzione in un ambiente specifico) nello stack, quindi lo rimuoverà e passerà al successivo.
Per l’esempio contenente solo codice sincrono, il browser gestisce l’esecuzione nel seguente ordine:
- Aggiungi
first()
allo stack, eseguifirst()
che registra1
sulla console, rimuovifirst()
dallo stack. - Aggiungi
second()
allo stack, eseguisecond()
che registra2
sulla console, rimuovisecond()
dallo stack. - Aggiungi
third()
allo stack, eseguithird()
che registra3
sulla console, rimuovithird()
dallo stack.
Il secondo esempio con setTimeout
appare così:
- Aggiungi
first()
allo stack, eseguifirst()
che registra1
sulla console, rimuovifirst()
dallo stack. - Aggiungi
second()
allo stack, eseguisecond()
.- Aggiungi
setTimeout()
allo stack, esegui ilsetTimeout()
Web API che avvia un timer e aggiunge la funzione anonima alla coda, rimuovisetTimeout()
dallo stack.
- Aggiungi
- Rimuovi
second()
dallo stack. - Aggiungi
third()
allo stack, eseguithird()
che registra3
sulla console, rimuovithird()
dallo stack. - Il ciclo degli eventi controlla la coda per eventuali messaggi in sospeso e trova la funzione anonima da
setTimeout()
, aggiunge la funzione allo stack che registra2
sulla console, quindi la rimuove dallo stack.
Utilizzando setTimeout
, un’API Web asincrona, si introduce il concetto della coda, che verrà trattato nel prossimo tutorial.
Coda
La coda, anche chiamata coda dei messaggi o coda dei compiti, è un’area di attesa per le funzioni. Ogni volta che lo stack delle chiamate è vuoto, il ciclo degli eventi controlla la coda per eventuali messaggi in attesa, partendo dal messaggio più vecchio. Una volta trovato, lo aggiunge allo stack, che eseguirà la funzione nel messaggio.
Nell’esempio di setTimeout
, la funzione anonima viene eseguita immediatamente dopo il resto dell’esecuzione di livello superiore, poiché il timer è stato impostato su 0
secondi. È importante ricordare che il timer non significa che il codice verrà eseguito esattamente dopo 0
secondi o dopo il tempo specificato, ma che aggiungerà la funzione anonima alla coda in quel periodo di tempo. Questo sistema di code esiste perché se il timer dovesse aggiungere la funzione anonima direttamente allo stack quando il timer finisce, interromperebbe qualsiasi funzione in esecuzione al momento, il che potrebbe avere effetti non intenzionali e imprevedibili.
Nota: Esiste anche un’altra coda chiamata coda dei compiti o coda delle microtask che gestisce le promesse. Le microtask come le promesse vengono gestite con una priorità più elevata rispetto alle macrotask come setTimeout
.
Ora sai come il ciclo degli eventi utilizza lo stack e la coda per gestire l’ordine di esecuzione del codice. Il compito successivo è capire come controllare l’ordine di esecuzione nel tuo codice. Per farlo, imparerai prima il modo originale per garantire che il codice asincrono venga gestito correttamente dal ciclo degli eventi: le funzioni di callback.
Funzioni di Callback
Nell’esempio setTimeout
, la funzione con il timeout è stata eseguita dopo tutto nel contesto di esecuzione principale di livello superiore. Ma se volessi garantire che una delle funzioni, come la funzione third
, venisse eseguita dopo il timeout, allora dovresti utilizzare metodi di codifica asincrona. Il timeout qui può rappresentare una chiamata API asincrona che contiene dati. Vuoi lavorare con i dati dalla chiamata API, ma devi assicurarti che i dati vengano restituiti prima.
La soluzione originale per affrontare questo problema è utilizzare funzioni di callback. Le funzioni di callback non hanno una sintassi speciale; sono solo una funzione che è stata passata come argomento a un’altra funzione. La funzione che prende un’altra funzione come argomento è chiamata una funzione di ordine superiore. Secondo questa definizione, qualsiasi funzione può diventare una funzione di callback se viene passata come argomento. Le callback non sono asincrone per natura, ma possono essere utilizzate per scopi asincroni.
Ecco un esempio di codice sintattico di una funzione di ordine superiore e una callback:
In questo codice, si definisce una funzione fn
, si definisce una funzione higherOrderFunction
che prende una funzione callback
come argomento, e si passa fn
come callback a higherOrderFunction
.
Eseguendo questo codice otterrai il seguente risultato:
OutputJust a function
Torniamo alle prime
, seconde
e terze
funzioni con setTimeout
. Ecco cosa hai finora:
Il compito è far sì che la funzione terza
ritardi sempre l’esecuzione fino a dopo che l’azione asincrona nella funzione seconda
è stata completata. Qui entrano in gioco i callback. Invece di eseguire prima
, seconda
e terza
a livello di esecuzione superiore, passerai la funzione terza
come argomento a seconda
. La funzione seconda
eseguirà il callback dopo che l’azione asincrona è stata completata.
Ecco le tre funzioni con un callback applicato:
Ora, esegui prima
e seconda
, poi passa terza
come argomento a seconda
:
Dopo aver eseguito questo blocco di codice, riceverai il seguente output:
Output1
2
3
Prima verrà stampato 1
, e dopo che il timer è completato (in questo caso, zero secondi, ma puoi cambiarlo a qualsiasi valore) verrà stampato 2
e poi 3
. Passando una funzione come callback, hai ritardato con successo l’esecuzione della funzione fino a quando l’API Web asincrona (setTimeout
) è completata.
Il punto chiave qui è che le funzioni di callback non sono asincrone – setTimeout
è l’API Web asincrona responsabile della gestione dei compiti asincroni. La funzione di callback ti consente solo di essere informato quando un compito asincrono è completato e gestisce il successo o il fallimento del compito.
Ora che hai imparato come utilizzare le callback per gestire compiti asincroni, la sezione successiva spiega i problemi di nidificazione di troppe callback e la creazione di una “piramide del terrore”.
Callback Nidificate e la Piramide del Terrore
Le funzioni di callback sono un modo efficace per garantire l’esecuzione ritardata di una funzione fino a quando un’altra non viene completata e restituisce dati. Tuttavia, a causa della natura nidificata delle callback, il codice può diventare disordinato se hai molte richieste asincrone consecutive che dipendono l’una dall’altra. Questo è stato un grande problema per gli sviluppatori JavaScript all’inizio e, di conseguenza, il codice contenente callback nidificate è spesso chiamato “piramide del terrore” o “inferno delle callback”.
Ecco una dimostrazione di callback nidificate:
In questo codice, ogni nuovo setTimeout
è nidificato all’interno di una funzione di ordine superiore, creando una forma piramidale di callback sempre più profonde. Eseguire questo codice produrrebbe quanto segue:
Output1
2
3
Nella pratica, con del codice asincrono nel mondo reale, questo può diventare molto più complicato. Molto probabilmente avrai bisogno di gestire gli errori nel codice asincrono e quindi passare alcuni dati da ogni risposta alla richiesta successiva. Farlo con i callback renderà il tuo codice difficile da leggere e mantenere.
Ecco un esempio eseguibile di un “piramide dell’orrore” più realistico con cui puoi sperimentare:
In questo codice, devi far sì che ogni funzione tenga conto di una possibile response
e un possibile error
, rendendo la funzione callbackHell
visivamente confusionaria.
Eseguendo questo codice otterrai quanto segue:
Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13
Questo modo di gestire il codice asincrono è difficile da seguire. Di conseguenza, è stata introdotta in ES6 il concetto di promesse. Questo è l’argomento della prossima sezione.
Promesse
A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.
Creazione di una Promise
Puoi inizializzare una promise con la sintassi new Promise
, e devi inizializzarla con una funzione. La funzione passata a una promise ha i parametri resolve
e reject
. Le funzioni resolve
e reject
gestiscono rispettivamente il successo e il fallimento di un’operazione.
Scrivi la seguente riga per dichiarare una promise:
Se ispezioni la promise inizializzata in questo stato con la console del tuo browser web, scoprirai che ha uno stato pending
e un valore undefined
:
Output__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
Fino a questo momento, nulla è stato configurato per la promise, quindi rimarrà in uno stato pending
per sempre. La prima cosa che puoi fare per testare una promise è soddisfare la promise risolvendola con un valore:
Ora, ispezionando la promise, scoprirai che ha uno stato fulfilled
e un value
impostato sul valore che hai passato a resolve
:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "We did it!"
Come indicato all’inizio di questa sezione, una promise è un oggetto che può restituire un valore. Dopo essere stata soddisfatta con successo, il value
passa da undefined
a essere popolato con dati.
A promise can have three possible states: pending, fulfilled, and rejected.
- Pending – Stato iniziale prima di essere risolta o rigettata
- Soddisfatto – Operazione riuscita, promessa risolta
- Rifiutato – Operazione fallita, promessa respinta
Dopo essere stata soddisfatta o rifiutata, una promessa è risolta.
Ora che hai un’idea di come vengono create le promesse, vediamo come uno sviluppatore potrebbe consumare queste promesse.
Consumare una Promessa
La promessa nell’ultima sezione è stata soddisfatta con un valore, ma si vuole anche essere in grado di accedere al valore. Le promesse hanno un metodo chiamato then
che verrà eseguito dopo che una promessa raggiunge lo stato resolve
nel codice. then
restituirà il valore della promessa come parametro.
Ecco come si restituirebbe e registrerebbe il valore
della promessa di esempio:
La promessa che hai creato aveva un [[PromiseValue]]
di Ci siamo riusciti!
. Questo valore è quello che verrà passato nella funzione anonima come risposta
:
OutputWe did it!
Fino ad ora, l’esempio che hai creato non ha coinvolto una API Web asincrona: ha solo spiegato come creare, risolvere e consumare una promessa JavaScript nativa. Utilizzando setTimeout
, puoi testare una richiesta asincrona.
Il seguente codice simula i dati restituiti da una richiesta asincrona come promessa:
Utilizzando la sintassi then
si assicura che la response
venga registrata solo quando l’operazione setTimeout
è completata dopo 2000
millisecondi. Tutto questo viene fatto senza nidificare le callback.
Ora, dopo due secondi, risolverà il valore della promise e verrà registrato in then
:
OutputResolving an asynchronous request!
Le Promises possono anche essere concatenate per passare i dati a più di un’operazione asincrona. Se un valore viene restituito in then
, può essere aggiunto un altro then
che si completerà con il valore restituito dal then
precedente:
La response soddisfatta nel secondo then
registrerà il valore restituito:
OutputResolving an asynchronous request! And chaining!
Dato che then
può essere concatenato, consente al consumo delle promises di apparire più sincrono rispetto alle callback, poiché non è necessario nidificarle. Questo consentirà di scrivere codice più leggibile che può essere mantenuto e verificato più facilmente.
Gestione degli errori
Finora hai gestito solo una promessa con un resolve
di successo, che mette la promessa in uno stato fulfilled
. Ma spesso, con una richiesta asincrona, devi anche gestire un errore – se l’API non è disponibile, o viene inviata una richiesta malformata o non autorizzata. Una promessa dovrebbe essere in grado di gestire entrambi i casi. In questa sezione, creerai una funzione per testare sia il caso di successo che quello di errore nella creazione e nel consumo di una promessa.
Questa funzione getUsers
passerà un flag a una promessa e restituirà la promessa:
Configura il codice in modo che se onSuccess
è true
, il timeout verrà soddisfatto con alcuni dati. Se è false
, la funzione verrà rigettata con un errore:
Per il risultato positivo, restituisci oggetti JavaScript che rappresentano dati utente di esempio.
Per gestire l’errore, utilizzerai il metodo di istanza catch
. Questo ti fornirà una callback di errore con error
come parametro.
Esegui il comando getUser
con onSuccess
impostato su false
, utilizzando il metodo then
per il caso di successo e il metodo catch
per l’errore:
Dato che l’errore è stato scatenato, il then
verrà saltato e il catch
gestirà l’errore:
OutputFailed to fetch data!
Se si cambia il flag e si usa resolve
invece, il catch
verrà ignorato e i dati verranno restituiti:
Questo restituirà i dati dell’utente:
Output(3) [{…}, {…}, {…}]
0: {id: 1, name: "Jerry"}
1: {id: 2, name: "Elaine"}
3: {id: 3, name: "George"}
A titolo informativo, ecco una tabella con i metodi di gestione sugli oggetti Promise
:
Method | Description |
---|---|
then() |
Handles a resolve . Returns a promise, and calls onFulfilled function asynchronously |
catch() |
Handles a reject . Returns a promise, and calls onRejected function asynchronously |
finally() |
Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously |
Le promesse possono essere confuse, sia per i nuovi sviluppatori che per i programmatori esperti che non hanno mai lavorato in un ambiente asincrono prima d’ora. Tuttavia, come accennato, è molto più comune consumare promesse che crearle. Di solito, sarà un’API Web del browser o una libreria di terze parti a fornire la promessa, e tu dovrai solo consumarla.
Nella sezione finale sulle promesse, questo tutorial menzionerà un caso d’uso comune di un’API Web che restituisce promesse: l’API Fetch.
Utilizzo dell’API Fetch con le Promesse
Uno dei Web API più utili e frequentemente utilizzati che restituisce una promessa è il Fetch API, che ti consente di effettuare una richiesta asincrona di risorse su una rete. fetch
è un processo in due parti e quindi richiede il chaining di then
. Questo esempio dimostra come effettuare una richiesta alla API di GitHub per recuperare i dati di un utente, gestendo anche eventuali errori potenziali:
La richiesta fetch
viene inviata all’URL https://api.github.com/users/octocat
, che attende asincronamente una risposta. Il primo then
passa la risposta a una funzione anonima che formatta la risposta come dati JSON, quindi passa il JSON a un secondo then
che registra i dati sulla console. La dichiarazione catch
registra eventuali errori sulla console.
Eseguendo questo codice si otterrà quanto segue:
Outputlogin: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
Questi sono i dati richiesti da https://api.github.com/users/octocat
, resi nel formato JSON.
Questa sezione del tutorial ha mostrato che le promesse incorporano molte migliorie per gestire il codice asincrono. Tuttavia, mentre l’uso di then
per gestire azioni asincrone è più facile da seguire rispetto alla piramide di callback, alcuni sviluppatori preferiscono ancora un formato sincrono per scrivere codice asincrono. Per affrontare questa esigenza, ECMAScript 2016 (ES7) ha introdotto le funzioni async
e la parola chiave await
per semplificare il lavoro con le promesse.
Funzioni Async con async/await
Una funzione async
ti permette di gestire il codice asincrono in modo che sembri sincrono. Le funzioni async
utilizzano comunque le promesse sotto il cofano, ma presentano una sintassi JavaScript più tradizionale. In questa sezione, proverai degli esempi di questa sintassi.
Puoi creare una funzione async
aggiungendo la parola chiave async
prima di una funzione:
Anche se questa funzione non sta gestendo nulla di asincrono al momento, si comporta in modo diverso rispetto a una funzione tradizionale. Se esegui la funzione, scoprirai che restituisce una promessa con un [[PromiseStatus]]
e [[PromiseValue]]
invece di un valore di ritorno.
Prova questo eseguendo un log della chiamata alla funzione getUser
:
Questo produrrà il seguente risultato:
Output__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: Object
Questo significa che puoi gestire una funzione async
con then
nello stesso modo in cui potresti gestire una promessa. Prova con il seguente codice:
Questa chiamata a getUser
passa il valore di ritorno a una funzione anonima che lo registra sulla console.
Riceverai il seguente risultato quando esegui questo programma:
Output{}
Una funzione async
può gestire una promessa chiamata al suo interno utilizzando l’operatore await
. await
può essere utilizzato all’interno di una funzione async
e aspetterà finché una promessa non si risolve prima di eseguire il codice designato.
Con questa conoscenza, è possibile riscrivere la richiesta Fetch dalla sezione precedente utilizzando async
/await
come segue:
Gli operatori await
qui garantiscono che i dati
non vengano registrati prima che la richiesta li abbia popolati con i dati.
Ora i dati finali possono essere gestiti all’interno della funzione getUser
, senza alcuna necessità di utilizzare then
. Questo è l’output della registrazione dei dati
:
Outputlogin: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
Nota: In molti ambienti, async
è necessario per utilizzare await
—tuttavia, alcune nuove versioni di browser e Node consentono l’utilizzo di await
al livello superiore, il che ti permette di evitare di creare una funzione asincrona per incapsulare l’operazione di await
.
Infine, poiché stai gestendo la promessa soddisfatta all’interno della funzione asincrona, puoi anche gestire l’errore all’interno della funzione. Invece di utilizzare il metodo catch
con then
, userai il modello try
/catch
per gestire l’eccezione.
Aggiungi il seguente codice evidenziato:
Il programma passerà ora al blocco catch
se riceve un errore e registrerà quell’errore sulla console.
Il codice JavaScript asincrono moderno viene gestito più spesso con la sintassi async
/await
, ma è importante avere una conoscenza pratica di come funzionano le promesse, specialmente perché le promesse sono in grado di offrire funzionalità aggiuntive che non possono essere gestite con async
/await
, come combinare le promesse con Promise.all()
.
Nota: async
/await
possono essere riprodotti utilizzando generatori combinati con promesse per aggiungere maggiore flessibilità al tuo codice. Per saperne di più, consulta il nostro tutorial su Comprendere i Generatori in JavaScript.
Conclusione
Poiché le API Web spesso forniscono dati in modo asincrono, imparare come gestire il risultato delle azioni asincrone è una parte essenziale dello sviluppo JavaScript. In questo articolo, hai appreso come l’ambiente host utilizza il ciclo degli eventi per gestire l’ordine di esecuzione del codice con lo stack e la coda. Hai anche provato esempi di tre modi per gestire il successo o il fallimento di un evento asincrono, con i callback, le promesse e la sintassi async
/await
. Infine, hai utilizzato l’API Web Fetch per gestire azioni asincrone.
Per ulteriori informazioni su come il browser gestisce gli eventi paralleli, leggi il documento Modello di concorrenza e il ciclo degli eventi sulla Mozilla Developer Network. Se desideri approfondire JavaScript, torna alla nostra serie Come Codificare in JavaScript.