Articolo originale: JavaScript Async/Await Tutorial – Learn Callbacks, Promises, and Async/Await in JS by Making Ice Cream 🍧🍨🍦
Oggi realizzeremo e gestiremo una gelateria e allo stesso tempo impareremo JavaScript asincrono. Strada facendo imparerai a usare:
- Callback
- Promise
- async / await
Ecco quello di cui parleremo in questo articolo:
- Cos'è JavaScript asincrono?
- JavaScript sincrono vs asincrono
- Come funzionano i callback in JavaScript
- Come funzionano le promise in JavaScript
- Come funzionano async/await in JavaScript
Cos'è JavaScript asincrono?
Se vuoi realizzare progetti in modo efficiente, questo concetto fa per te.
La teoria di JavaScript asincrono ti aiuta a suddividere dei progetti grandi e complessi in attività più semplici.
Poi puoi utilizzare tre tecniche – callback, promise o async/await – per eseguire queste attività più semplici in modo da ottenere i risultati migliori.
Iniziamo!🎖️
JavaScript sincrono vs asincrono
Cos'è un sistema sincrono?
In un sistema sincrono, le operazioni vengono completate una dopo l'altra.
Immagina di avere una sola mano per svolgere 10 azioni, in modo da doverle completare una alla volta.
Dai un'occhiata alla GIF 👇 – le operazioni avvengono una alla volta:
Finché la prima immagine non è completamente caricata, non inizierà il caricamento della seconda.
JavaScript è sincrono (mono-thread) di default. Pensala così: un thread corrisponde a una mano con cui poter fare delle cose.
Cos'è un sistema asincrono?
In questo sistema, le operazioni sono svolte indipendentemente.
In questo caso, immagina che per le dieci azioni, hai 10 mani. Quindi, ogni mano può occuparsi indipendentemente di un'azione, in contemporanea alle altre.
Dai un'occhiata alla GIF 👇 – come puoi vedere, le immagini vengono caricate allo stesso tempo.
Di nuovo, tutte le immagini sono caricate secondo il proprio ritmo. Nessuna di loro aspetta le altre.
Per riassumere JS sincrono vs asincrono:
Quando tre immagini corrono una maratona:
- In un sistema sincrono, le tre immagini sono nella stessa corsia. Un'immagine non può superarne un'altra. Finiscono la corsa una dopo l'altra e se l'immagine numero 2 si ferma, si ferma anche l'immagine seguente:
- In un sistema asincrono, le tre immagini sono in corsie diverse. Finiscono la gara secondo il loro passo, senza doversi fermare a causa delle altre:
Esempi di codice sincrono e asincrono
Prima di iniziare con il nostro progetto, diamo un'occhiata ad alcuni esempi e togliamoci ogni dubbio.
Esempio di codice sincrono
Per testare un sistema sincrono, scrivi questo codice in JavaScript:
console.log(" Io ");
console.log(" mangio ");
console.log(" il gelato ");
Ecco il risultato nella console: 👇
Esempio di codice asincrono
Diciamo che ci vogliono due secondi per mangiare del gelato. Adesso, testiamo un sistema asincrono. Scrivi il codice JavaScript che trovi qui sotto.
Nota: non preoccuparti, parleremo della funzione setTimeout()
più avanti in questo articolo.
console.log(" Io ");
// Il blocco qui sotto verrà mostrato dopo due secondi
setTimeout(()=>{
console.log(" mangio ");
}, 2000);
console.log(" il gelato ")
Ed ecco il risultato nella console: 👇
Ora che sai la differenza tra le operazioni sincrone e asincrone, realizziamo la nostra gelateria.
Come impostare il progetto
Per questo progetto puoi aprire semplicemente Codepen.io e iniziare a scrivere il codice, oppure puoi usare VS code o un editor di tua scelta.
Apri la sezione JavaScript e poi la console da sviluppatore. Scriveremo il nostro codice e vedremo il risultato sulla console.
Cos'è un callback in JavaScript?
Un callback è una funzione usata come argomento di un'altra funzione.
Ecco un'illustrazione di un callback:
function callback(){
// fa qualcosa
}
function fun(param){
// fa qualcos'altro
param();
}
fun(callback);
Non preoccuparti, tra poco vedremo degli esempi di callback.
Perché usiamo i callback?
Quando svolgiamo delle operazioni complesse, le suddividiamo in passaggi più semplici. Per fare in modo di stabilire delle relazioni tra questi passaggi in base al tempo (opzionale) e all'ordine, usiamo i callback.
Guarda questo esempio:👇
Questi sono i passaggi elementari che devi svolgere per fare il gelato. Nota che in questo esempio, l'ordine dei passaggi e il tempo sono cruciali. Non puoi semplicemente tagliare la frutta e servire il gelato.
Allo stesso tempo, se il passaggio precedente non è completato, non puoi andare al successivo.
Per spiegarlo più dettagliatamente, iniziamo a mettere su la nostra gelateria.
Ma un momento...
L'attività sarà divisa in due parti:
- La dispensa che conterrà tutti gli ingredienti (il nostro back end)
- La cucina in cui produrremo il gelato (il front end)
Memorizziamo i nostri dati
Adesso, dobbiamo memorizzare i nostri ingredienti in un oggetto. Iniziamo!
Puoi salvare gli ingredienti all'interno di un oggetto, in questo modo: 👇
let ingredienti = {
frutta : ["fragola", "uva", "banana", "mela"]
};
Gli altri ingredienti sono: 👇
Puoi inserire gli altri ingredienti in un oggetto JavaScript così: 👇
let ingredienti = {
frutta : ["fragola", "uva", "banana", "mela"],
contenitori : ["cono", "coppetta", "stecco"],
topping : ["cioccolato", "granella"],
altro : ["acqua", "ghiaccio"]
};
L'intera attività dipende dagli ordini dei clienti. Una volta che abbiamo un ordine, iniziamo la produzione e poi serviamo il gelato. Quindi, creeremo due funzioni ->
ordine
preparazione
Ecco come funziona il tutto: 👇
Creiamo le nostre funzioni. Utilizzeremo delle funzioni freccia:
let ordine = () =>{};
let preparazione = () =>{};
Adesso, stabiliamo una relazione tra le due funzioni usando un callback, in questo modo: 👇
let ordine = (chiamaPrep) =>{
chiamaPrep();
};
let preparazione = () =>{};
Facciamo un piccolo test
Useremo la funzione console.log()
per fare dei test, in modo da sciogliere ogni dubbio che potremmo avere su come abbiamo stabilito la relazione tra le due funzioni.
let ordine = (chiamaPrep) =>{
console.log("Ordine effettuato. Chiama preparazione")
// la funzione 👇 chiamata
chiamaPrep();
};
let preparazione = () =>{
console.log("La preparazione è iniziata")
};
Per eseguire il test, chiameremo la funzione ordine
. E aggiungeremo la seconda funzione, chiamata preparazione
come suo argomento.
// nome 👇 della seconda funzione
ordine(preparazione);
Ecco il risultato sulla console: 👇
Fai una pausa
Finora tutto bene – fai una pausa!
Ripuliamo il console.log
Tieni il codice e rimuovi il resto (non cancellare la variabile ingredienti
). Nella prima funzione, passiamo un altro argomento in modo da poter ricevere l'ordine (il nome della frutta):
// funzione 1
let ordine = (nomeFrutta, chiamaPrep) =>{
chiamaPrep();
};
// funzione 2
let preparazione = () =>{};
// chiamata 👇
ordine("", preparazione);
Ecco i nostri passaggi e il tempo necessario per ognuno di essi.
In questo diagramma, puoi vedere che il primo punto è effettuare l'ordine, per il quale servono 2 secondi. Il secondo punto è tagliare la frutta (2 secondi), il terzo è aggiungere acqua e ghiaccio (1 secondo), il quarto è avviare il macchinario (1 secondo), il quinto è selezionare il contenitore (2 secondi), il sesto è scegliere il topping (3 secondi) e il settimo e ultimo è servire il gelato (2 secondi).
Per stabilire il tempo, la funzione setTimeout()
è perfetta, in quanto utilizza un callback prendendo una funzione come argomento.
Adesso, scegliamo la frutta e utilizziamo la funzione:
// funzione 1
let ordine = (nomeFrutta, chiamaPrep) =>{
setTimeout(function(){
console.log(`${ingredienti.frutta[nomeFrutta]} è stata selezionata`)
// Ordine effettuato. Chiama preparazione per iniziare
chiamaPrep();
},2000)
};
// funzione 2
let preparazione = () =>{
// vuota per ora
};
// chiamata 👇
ordine(0, preparazione);
Ed ecco il risultato sulla console: 👇
Nota che il risultato viene mostrato dopo 2 secondi.
Se ti stai chiedendo come abbiamo scelto la fragola dalla variabile ingredienti, ecco la spiegazione: 👇
Non cancellare nulla. Adesso inizieremo a scrivere la funzione preparazione con il seguente codice.👇 Useremo delle funzioni freccia:
let preparazione = () =>{
setTimeout(()=>{
console.log("la preparazione è iniziata")
},0000)
};
Ed ecco il risultato: 👇
Annideremo un'altra funzione setTimeout
nella funzione setTimeout
esistente per tagliare la frutta. In questo modo: 👇
let preparazione = () =>{
setTimeout(()=>{
console.log("la preparazione è iniziata");
setTimeout(()=>{
console.log("la frutta è stata tagliata");
},2000);
},0000);
};
Ed ecco il risultato: 👇
Se ricordi, questa è la nostra scaletta:
Completiamo la preparazione del gelato annidando una funzione all'interno di un'altra – si tratta di un callback, ricordi?
let preparazione = () =>{
setTimeout(()=>{
console.log("la preparazione è iniziata")
setTimeout(()=>{
console.log("la frutta è stata tagliata")
setTimeout(()=>{
console.log(`${ingredienti.altro[0]} e ${ingredienti.altro[1]} aggiunti`)
setTimeout(()=>{
console.log("avvio macchina")
setTimeout(()=>{
console.log(`gelato messo su ${ingredienti.contenitori[1]}`)
setTimeout(()=>{
console.log(`${ingredienti.topping[0]} come topping`)
setTimeout(()=>{
console.log("gelato servito")
},2000)
},3000)
},2000)
},1000)
},1000)
},2000)
},0000)
};
Ed ecco il risultato sulla console: 👇
Ti senti confuso?
Questo si chiama "inferno callback". Assomiglia a qualcosa del genere (hai presente il codice qui sopra?): 👇
Come possiamo risolverlo?
Come usare le promise per sfuggire all'inferno callback
Le promise sono state inventate per risolvere il problema dell'inferno callback e per gestire meglio le operazioni.
Fai una pausa
Ma prima, fai una pausa!
Una promise ha questo aspetto:
Analizziamo insieme le promise.
Come mostrato dal diagramma qui sopra, una promise ha tre stati:
- Pending: è lo stato iniziale. Nulla accade qui. È come se il tuo cliente stesse prendendo il suo tempo per ordinare, ma non ha ancora ordinato nulla.
- Resolved (risolta): vuol dire che il cliente ha ricevuto il suo cibo ed è contento.
- Rejected (respinta): vuol dire che il cliente non ha ricevuto il proprio ordine e ha lasciato il ristorante.
Usiamo le promise per il caso di studio della preparazione del nostro gelato.
Ma un momento...
Dobbiamo prima comprendere altre quattro cose ->
- La relazione tra tempo e lavoro
- Il concatenamento di promise
- La gestione degli errori
- Il gestore
.finally
Iniziamo a creare la nostra gelateria e cerchiamo di comprendere ognuno di questi concetti, uno alla volta e a piccoli passi.
La relazione tra tempo e lavoro
Se ricordi, questi sono i passaggi per preparare il gelato, con il tempo necessario per ognuno.
Per renderlo possible, creiamo una variabile in JavaScript:👇
let gelateriaAperta = true;
Adesso creiamo una funzione chiamata ordine
a cui passiamo due argomenti chiamati tempo
e lavoro
:
let ordine = ( lavoro, tempo ) =>{
}
Ora, faremo una promessa al nostro cliente: "Ti serviremo il gelato"
In questo modo ->
let ordine = ( lavoro, tempo ) =>{
return new Promise( ( resolve, reject )=>{ } )
}
La promise è composta da due parti:
- Risolta [ gelato servito ]
- Respinta [ il cliente non ha avuto il gelato ]
let ordine = ( lavoro, tempo ) => {
return new Promise( ( resolve, reject )=>{
if( gelateriaAperta ){
resolve( )
}
else{
reject( console.log("La gelateria è chiusa") )
}
})
}
Aggiungiamo i fattori tempo e lavoro nella nostra promise usando una funzione setTimeout()
all'interno dell'istruzione if
. Seguimi 👇
Nota: neanche nella vita reale puoi evitare il fattore tempo. Ciò è completamente dipendente dalla natura del tuo lavoro.
let ordine = ( lavoro, tempo ) => {
return new Promise( ( resolve, reject )=>{
if( gelateriaAperta ){
setTimeout(()=>{
// il lavoro 👇 viene svolto qui
resolve( lavoro() )
// impostiamo 👇 qui il tempo per il lavoro
}, tempo)
}
else{
reject( console.log("La gelateria è chiusa") )
}
})
}
Utilizzeremo la funzione appena creata per iniziare la preparazione del gelato.
//passa 👇 qui una funzione per iniziare il lavoro
ordine(()=>console.log(`${ingredienti.frutta[0]} è stata selezionata`), 2000)
// imposta qui il tempo ☝️
Il risultato 👇 dopo 2 secondi sarà questo:
Ottimo lavoro!
Il concatenamento delle promise
Per questo metodo, definiamo ciò che dobbiamo fare quando la prima attività è completa usando il gestore .then
. Qualcosa del genere 👇
Il gestore .then
restituisce una promise quando la promise originale viene risolta.
Ecco un esempio:
Facciamola semplice: è un po' come dare delle istruzioni a qualcuno. Dici a qualcuno "Fai prima questo, poi quest'altra cosa, poi questa, poi..." e così via.
- La prima operazione è la promise originale.
- Il resto delle operazioni restituisce la nostra promise una volta che una piccola porzione di codice è completata
Implementiamolo nel nostro progetto. In fondo al nostro codice scrivi le seguenti righe. 👇
Nota: non dimenticare di scrivere la parola return
all'interno del gestore .then
. Altrimenti non funzionerà appropriatamente. Se sei curioso, prova a rimuovere il return
una volta terminati questi passaggi:
ordine(()=>console.log(`${ingredienti.frutta[0]} è stata selezionata`), 2000)
.then(()=>{
return ordine(()=>console.log('la preparazione è iniziata'), 000)
})
Ed ecco il risultato: 👇
Finiamo il nostro progetto usando lo stesso sistema:👇
// step 1
ordine(()=>console.log(`${ingredienti.frutta[0]} è stata selezionata`), 2000)
// step 2
.then(()=>{
return ordine(()=>console.log('la produzione è iniziata'), 000)
})
// step 3
.then(()=>{
return ordine(()=>console.log("la frutta è stata tagliata"), 2000)
})
// step 4
.then(()=>{
return ordine(()=>console.log(`${ingredienti.altro[0]} e ${ingredienti.altro[1]} aggiunti`), 1000)
})
// step 5
.then(()=>{
return ordine(()=>console.log("avvio macchina"), 1000)
})
// step 6
.then(()=>{
return ordine(()=>console.log(`gelato messo su ${ingredienti.contenitori[1]}`), 2000)
})
// step 7
.then(()=>{
return ordine(()=>console.log(`${ingredienti.topping[0]} come topping`), 3000)
})
// Step 8
.then(()=>{
return ordine(()=>console.log("gelato servito"), 2000)
})
Il risultato è: 👇
La gestione degli errori
Ci serve un modo per gestire gli errori quando qualcosa va storto, ma prima, dobbiamo capire il ciclo di una promise:
Per individuare gli errori, cambiamo il valore della nostra variabile in false
.
let gelateriaAperta = false;
Questo vuol dire che la gelateria è chiusa. Non stiamo più vendendo gelato ai nostri clienti.
Per gestire questa situazione, usiamo il gestore .catch
. Proprio come .then
, restituisce anch'esso una promise, ma solo quando la promise originale è respinta.
Un piccolo promemoria:
.then
funziona quando una promise è risolta (resolved).catch
funziona quando una promise è respinta (rejected)
Vai in fondo e scrivi il seguente codice:👇
Ricorda soltanto che non dovrebbe esserci nulla tra il gestore .then
precedente e il gestore .catch
.
.catch(()=>{
console.log("il cliente è andato via")
})
Questo è il risultato:👇
Un paio di cose da notare su questo codice:
- Il primo messaggio arriva dalla parte
reject()
della promise - Il secondo messaggio arriva dal gestore
.catch
Come usare il gestore .finally()
Il gestore .finally
funziona indipendentemente dal fatto che la promise sia risolta o respinta.
Ad esempio: possiamo servire 100 clienti o nessuno, ma la gelateria chiuderà comunque a fine giornata.
Se sei curioso, vai alla fine del codice e scrivi questo: 👇
.finally(()=>{
console.log("fine giornata")
})
Il risultato:👇
E adesso, date tutti il benvenuto ad async/await.
Come funzionano async/await in JavaScript?
Questo dovrebbe essere il modo migliore per scrivere promise e ci aiuta a mantenere il codice semplice e pulito.
Tutto ciò che devi fare è scrivere la parola async
prima di una funzione regolare per farla diventare una promise.
Ma prima, fai una pausa!
Dai un'occhiata qui:👇
Promise vs async/await in JavaScript
Prima di async/await, per creare una promise avremmo scritto questo:
function ordine(){
return new Promise( (resolve, reject) =>{
// codice
} )
}
Invece, usando async/await, possiamo farlo in questo modo:
//👇 la parola chiave magica
async function ordine() {
// codice
}
Un momento...
Dobbiamo capire ->
- Come usare le parole chiave
try
ecatch
- Come usare la parola chiave
await
Come usare le parole chiave try
e catch
Usiamo la parola chiave try
per eseguire il codice, mentre usiamo catch
in caso di errori. È lo stesso concetto che abbiamo visto per le promise.
Facciamo un confronto. Vedremo una piccola dimostrazione della sintassi, e poi inizieremo a scrivere del codice.
Promise in JS -> resolve o reject
Risolviamo o respingiamo una promise in questo modo:
function cucina(){
return new Promise ((resolve, reject)=>{
if(true){
resolve("la promise è soddisfatta")
}
else{
reject("c'è un errore")
}
})
}
cucina() // esegui codice
.then() // prossimo step
.then() // prossimo step
.catch() // c'è un errore
.finally() // fine della promise [optionale]
async / await in JS -> try, catch
Quando usiamo async/await, lo facciamo con questo formato:
//👇 parola chiave magica
async function cucina(){
try{
// creiamo un problema fasullo
await abc;
}
catch(errore){
console.log("abc non esiste", errore)
}
finally{
console.log("il codice viene eseguito comunque")
}
}
cucina() // esegue codice
Niente panico, adesso parleremo della parola chiave await
.
Come usare la parola chiave await
in JS
La parola chiave await
fa sì che JavaScript attenda fino a che il risultato della promise non è stabilito e lo restituisce.
Torniamo alla nostra gelateria. Non sappiamo quale topping preferisce un cliente – cioccolato o granella. Quindi abbiamo bisogno di stoppare il macchinario e andare a chiedere al cliente cosa preferisce sul suo gelato.
Nota che soltanto la cucina si è fermata, ma lo staff fuori dalla cucina sta ancora facendo cose come:
- impiattare
- pulire i tavoli
- prendere ordini e così via.
Esempio di codice con la parola chiave await
Creiamo una piccola promise per chiedere quale topping usare. Il processo impiega tre secondi.
function sceltaTopping (){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve( console.log("quale topping vorresti?") )
},3000)
})
}
Ora, creiamo la funzione cucina
con la parola chiave async
davanti.
async function cucina(){
console.log("A")
console.log("B")
console.log("C")
await sceltaTopping()
console.log("D")
console.log("E")
}
// chiamata della funzione
cucina();
Aggiungiamo altre operazioni sotto la chiamata di cucina()
.
console.log("impiattare")
console.log("pulire i tavoli")
console.log("prendere ordini")
Ed ecco il risultato:
Stiamo letteralmente andando fuori dalla cucina per chiedere al cliente "Quale topping vorresti?". Nel frattempo, vengono svolte altre operazioni.
Una volta che sappiamo quale topping ha scelto, entriamo in cucina e terminiamo il lavoro.
Una piccola nota
Insieme ad async/ await, puoi usare anche i gestori .then
, .catch
e .finally
, che sono una parte centrale delle promise.
Apriamo di nuovo la gelateria
Creeremo due funzioni ->
cucina
: per preparare il gelatotempo
: per assegnare il tempo necessario a svolgere ogni attività.
Partiamo dalla funzione time:
let gelateriaAperta = true;
function tempo(ms) {
return new Promise( (resolve, reject) => {
if(gelateriaAperta){
setTimeout(resolve, ms);
}
else{
reject(console.log("la gelateria è chiusa"))
}
});
}
E ora, la funzione cucina
:
async function cucina(){
try{
// istruzioni
}
catch(errore){
// gestione degli errori
}
}
// chiamata
cucina();
Scriviamo delle piccole istruzioni e testiamo se la funzione cucina
sta funzionando oppure no:
async function cucina(){
try{
// tempo necessario per la prima operazione
await tempo(2000)
console.log(`${ingredienti.frutta[0]} è stata selezionata`)
}
catch(errore){
console.log("il cliente è andato via")
}
finally{
console.log("fine giornata, la gelateria chiude")
}
}
// chiamata
cucina();
Quando la gelateria è aperta, il risultato è simile a questo: 👇
Ed ecco il risultato quando la gelateria è chiusa: 👇
Per ora tutto bene.
Completiamo il nostro progetto.
Ecco di nuovo la lista delle operazioni: 👇
Prima di tutto apriamo la gelateria:
let gelateriaAperta = true;
Ora scriviamo i passaggi nella funzione cucina()
: 👇
async function cucina(){
try{
await tempo(2000)
console.log(`${ingredienti.frutta[0]} è stata selezionata`)
await tempo(0000)
console.log("la preparazione è iniziata")
await tempo(2000)
console.log("la frutta è stata tagliata")
await tempo(1000)
console.log(`${ingredienti.altro[0]} e ${ingredienti.altro[1]} aggiunti`)
await tempo(1000)
console.log("avvio macchina")
await tempo(2000)
console.log(`gelato messo su ${ingredienti.contenitori[1]}`)
await tempo(3000)
console.log(`${ingredienti.topping[0]} come topping`)
await tempo(2000)
console.log("gelato servito")
}
catch(error){
console.log("il cliente è andato via")
}
}
Ed ecco il risultato: 👇
Conclusione
Grazie per aver letto questo articolo fino alla fine! In questo tutorial hai imparato:
- La differenza tra sistemi sincroni e asincroni
- I meccanismi alla base di JavaScript asincrono, usando 3 tecniche (callback, promise e async/await)
Ed ecco la tua medaglia per aver letto fine alla fine. ❤️