Articolo originale: Async Await JavaScript Tutorial – How to Wait for a Function to Finish in JS
Quando termina una funzione asincrona? E perché è una domanda così difficile a cui rispondere?
Bene, sembra che la comprensione delle funzioni asincrone richieda una approfondita conoscenza di come funziona internamente JavaScript.
Esploriamo questo concetto e impariamo molto su questo processo in JavaScript .
Siete pronti? Andiamo.
Cos'è il codice asincrono?
Per come è progettato, JavaScript è un linguaggio di programmazione sincrono. Ciò significa che quando il codice viene eseguito, JavaScript inizia dalla parte superiore del file ed esegue il codice riga per riga, fino al completamento.
Il risultato di questa decisione progettuale è che può essere eseguita solo una cosa alla volta.
Puoi pensare a questo come se stessi facendo il giocoliere con sei palline. Mentre le fai roteare in aria, le tue mani sono occupate e non puoi gestire nient'altro.
È lo stesso con JavaScript: una volta che il codice è in esecuzione, ha le mani piene con quel codice. Si definisce bloccante questo tipo di codice sincrono. Perché sta effettivamente bloccando l'esecuzione di altro codice.
Torniamo all'esempio di giocoleria. Cosa accadrebbe se volessi aggiungere un'altra palla? Invece di sei palle, ti piacerebbe giocare con sette palle. Questo potrebbe essere un problema.
Non vuoi smettere di fare il giocoliere, perché è così divertente. Ma non puoi nemmeno andare a prendere un'altra palla, perché ciò significherebbe che dovresti fermarti.
La soluzione? Delega il lavoro a un amico o un familiare. Non stanno facendo il giocoliere, quindi possono andare a prendere la palla per te, lanciartela in un momento in cui la tua mano è libera e quindi pronto per aggiungerla in mezzo.
Questo è il codice asincrono. JavaScript sta delegando il lavoro a qualcos'altro, mentre fa gli affari suoi. Poi, quando sarà pronto, riceverà i risultati del lavoro.
Chi fa l'altro lavoro?
Va bene, quindi sappiamo che JavaScript è sincrono e pigro. Non vuole fare tutto il lavoro da solo, quindi lo trasforma in qualcos'altro.
Ma chi è questa misteriosa entità che lavora per JavaScript? E come viene assunto per funzionare per JavaScript?
Bene, diamo un'occhiata a un esempio di codice asincrono.
const logName = () => {
console.log("Han")
}
setTimeout(logName, 0)
console.log("Hey ciao")
L'esecuzione di questo codice genera il seguente output nella console:
// in console
Hey ciao
Han
Bene. Cosa sta succedendo?
Pare che il modo in cui eseguiamo il lavoro in JavaScript consiste nell'utilizzare funzioni e API specifiche dell'ambiente. E questa è fonte di grande confusione in JavaScript.
JavaScript viene sempre eseguito in un ambiente.
Spesso, quell'ambiente è il browser. Ma può anche essere sul server con NodeJS. Ma qual è la differenza?
La differenza – e questo è importante – è che il browser e il server (NodeJS), dal punto di vista delle funzionalità, non sono equivalenti. Spesso sono simili, ma non sono la stessa cosa.
Illustriamo questo con un esempio. Diciamo che JavaScript è il protagonista di un libro fantasy epico. Supponiamo sia un normale ragazzo di campagna.
Ora diciamo che questo ragazzo della fattoria ha trovato due armature speciali che gli hanno conferito poteri particolari.
Quando utilizza l'armatura del browser, ottiene l'accesso a un certo insieme di funzionalità.
Quando utilizza l'armatura del server ottiene l'accesso a un altro insieme di capacità.
Queste armature hanno alcune sovrapposizioni, perché i loro creatori avevano le stesse esigenze in certe situazioni, ma non in altre.
Ecco cos'è un ambiente. Un luogo in cui viene eseguito il codice, in cui esistono strumenti basati sul linguaggio JavaScript esistente. Non fanno parte del linguaggio, ma la linea è spesso sottile perché utilizziamo questi strumenti ogni giorno quando scriviamo il codice.
setTimeout , fetch e DOM sono tutti esempi di API Web. (Puoi vedere l'elenco completo delle API Web qui. ) Sono strumenti integrati nel browser e resi disponibili quando il nostro codice viene eseguito.
E poiché eseguiamo sempre JavaScript in un ambiente, sembra che questi facciano parte del linguaggio. Ma non lo sono.
Quindi, se ti sei mai chiesto perché puoi usare fetch in JavaScript quando lo esegui nel browser (ma devi installare un pacchetto quando lo esegui in NodeJS), ecco perché. Qualcuno ha pensato che il fetch fosse una buona idea e l'ha costruito come strumento per l'ambiente NodeJS.
Confuso? Sì!
Ma ora possiamo finalmente capire cosa prende il lavoro da JavaScript e come viene arruolato.
Si scopre che è l'ambiente che si fa carico del lavoro, e il modo per far si che l'ambiente faccia quel lavoro, è usare la funzionalità che appartiene all'ambiente. Ad esempio fetch o setTimeout nell'ambiente del browser.
Cosa succede al lavoro?
Grande. Quindi l'ambiente si fa carico del lavoro. E poi?
Ad un certo punto è necessario recuperare i risultati. Ma pensiamo a come potrebbe funzionare.
Torniamo all'esempio di giocoleria dell'inizio. Immagina di aver chiesto una nuova palla e un amico ha appena iniziato a lanciarti la palla quando non eri pronto.
Sarebbe un disastro. Forse potresti essere fortunato, prenderla e inserirla nella tua routine in modo efficace. Ma c'è una forte possibilità che possa farti cadere tutte le palle e mandare in crash la tua routine. Non sarebbe meglio se dessi istruzioni precise su quando ricevere la palla?
Effettivamente, ci sono regole rigide relative al momento in cui JavaScript può ricevere il risultato del lavoro delegato.
Tali regole sono regolate dal ciclo degli eventi e coinvolgono la coda del microtask e del macrotask. Si, lo so. È molto. Ma abbi pazienza con me.

Bene. Quindi, quando deleghiamo codice asincrono al browser, il browser prende ed esegue il codice e si assume quel carico di lavoro. Ma potrebbero esserci più attività che vengono assegnate al browser, quindi dobbiamo assicurarci di poter dare priorità a queste attività.
È qui che entrano in gioco la coda del microtask e la coda del macrotask. Il browser prenderà il lavoro, lo farà, quindi collocherà il risultato in una delle due code in base al tipo di lavoro che riceve.
Le promesse, ad esempio, vengono inserite nella coda del microtask e hanno una priorità più alta.
Eventi e setTimeout sono esempi di lavoro che viene inserito nella coda delle macrotask e hanno una priorità inferiore.
Ora, una volta che il lavoro è terminato e viene posizionato in una delle due code, il ciclo di eventi verrà eseguito avanti e indietro e verificherà se JavaScript è pronto o meno per ricevere i risultati.
Solo quando JavaScript ha finito di eseguire tutto il suo codice sincrono ed è pronto e funzionante, il ciclo di eventi inizierà a prelevare dalle code e restituire le funzioni a JavaScript per l'esecuzione.
Quindi diamo un'occhiata a un esempio:
setTimeout(() => console.log("hello"), 0)
fetch("https://someapi/data").then(response => response.json())
.then(data => console.log(data))
console.log("Quale zuppa?")
Quale sarà l'ordine qui?
- In primo luogo, setTimeout è delegato al browser, che fa il lavoro e inserisce la funzione risultante nella coda del macrotask.
- In secondo luogo fetch è delegato al browser, che prende il lavoro. Recupera i dati dall'endpoint e inserisce le funzioni risultanti nella coda del microtask.
- Javascript stampa "Quale zuppa"?
- Il ciclo di eventi verifica se JavaScript è pronto o meno a ricevere i risultati dal lavoro in coda.
- Al termine di console.log, JavaScript è pronto. Il ciclo di eventi preleva le funzioni in coda dalla coda del microtask, che ha una priorità più alta, e le restituisce a JavaScript per l'esecuzione.
- Dopo che la coda del microtask è vuota, il callback setTimeout viene prelevato dalla coda del macrotask e restituito a JavaScript per l'esecuzione.
In console:
// Quale zuppa?
// dati dall'api
// hello
Promesse
Ora dovresti avere una buona conoscenza di come il codice asincrono viene gestito da JavaScript e dall'ambiente del browser. Allora parliamo di promesse.
Una promessa è un costrutto JavaScript che rappresenta un valore futuro sconosciuto. Concettualmente, una promessa è solo JavaScript che promette di restituire un valore . Potrebbe essere il risultato di una chiamata API o potrebbe essere un oggetto di errore da una richiesta di rete non riuscita. Hai la garanzia di ottenere qualcosa.
const promise = new Promise((resolve, reject) => {
// fa una richiesta di rete
if (response.status === 200) {
resolve(response.body)
} else {
const error = { ... }
reject(error)
}
})
promise.then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
Una promessa può avere i seguenti stati:
- adempiuto - azione completata con successo
- respinto - azione non riuscita
- in attesa - nessuna azione è stata completata
- risolto - è stato soddisfatto o rifiutato
Una promessa riceve una funzione di risoluzione e di rifiuto che può essere chiamata per attivare uno di questi stati.
Uno dei grandi punti di forza delle promesse è che possiamo concatenare le funzioni che vogliamo che vengano eseguite in caso di successo (risoluzione) o fallimento (rifiuto):
- Per registrare una funzione da eseguire con successo, utilizziamo .then
- Per registrare una funzione da eseguire in caso di errore utilizziamo .catch
// Fetch ritorna una promessa
fetch("https://swapi.dev/api/people/1")
.then((res) => console.log("Questa funzione viene eseguita se la richiesta ha buon esito", res)
.catch(err => console.log("Questa funzione viene eseguita quando la richiesta fallisce", err)
// Concatenamento di più funzioni
fetch("https://swapi.dev/api/people/1")
.then((res) => doSomethingWithResult(res))
.then((finalResult) => console.log(finalResult))
.catch((err => doSomethingWithErr(err))
Perfetto. Ora diamo un'occhiata più da vicino all'intero meccanismo, usando fetch come esempio:
const fetch = (url, options) => {
// semplificato
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// ... fa una richiesta
xhr.onload = () => {
const options = {
status: xhr.status,
statusText: xhr.statusText
...
}
resolve(new Response(xhr.response, options))
}
xhr.onerror = () => {
reject(new TypeError("Request failed"))
}
}
fetch("https://swapi.dev/api/people/1")
// Registra handleResponse quando la promessa riesce
.then(handleResponse)
.catch(handleError)
// concettualmente, ora la promessa assomiglia a questo:
// { status: "pending", onsuccess: [handleResponse], onfailure: [handleError] }
const handleResponse = (response) => {
// handleResponse riceverà automaticamente la risposta, ¨
// perché la promessa si risolve con un valore e si inserisce automaticamente nella funzione
console.log(response)
}
const handleError = (response) => {
// handleError riceverà automaticamente l'errore, ¨
// perché la promessa si risolve con un valore e si inserisce automaticamente nella funzione
console.log(response)
}
// la promessa verrà risolta o rifiutata facendo in modo che esegua tutte le funzioni registrate nei rispettivi array
// iniettando il valore. Esaminiamo il percorso della logica:
// 1. Il listener di eventi XHR si attiva
// 2. Se la richiesta ha esito positivo, viene attivato il listener di eventi onload
// 3. L'onload attiva la funzione resolve(VALUE) con il valore dato
// 4. Risolve attiva e pianifica le funzioni registrate con .then
Quindi possiamo usare le promesse per fare un lavoro asincrono e per essere sicuri di poter gestire qualsiasi risultato da quelle promesse. Questo è il valore aggiunto. Se vuoi saperne di più sulle promesse puoi leggere di più su di esse qui e qui .
Quando utilizziamo le promesse, incateniamo le nostre funzioni alla promessa per gestire i diversi scenari.
Funziona, ma dobbiamo ancora gestire la nostra logica all'interno dei callback (funzioni nidificate) una volta recuperati i risultati. E se potessimo usare le promesse ma scrivere allo stesso tempo un codice dall'aspetto sincrono? Pare che sia possibile.
Async/Await
Async/Await è un modo di scrivere promesse che ci consente di scrivere codice asincrono in modo sincrono. Diamo un'occhiata.
const getData = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
const data = await response.json()
console.log(data)
}
getData()
Nulla è cambiato dietro le quinte qui. Stiamo ancora usando le promesse per recuperare i dati, ma ora sembra sincrono e non abbiamo più blocchi .then e .catch.
Async / Await è in realtà solo un fronzolo sintattico che fornisce un modo per creare codice su cui è più facile ragionare, senza modificare la dinamica sottostante.
Diamo un'occhiata a come funziona.
Async/Await ci consente di utilizzare i generatori per mettere in pausa l'esecuzione di una funzione. Quando utilizziamo async / await non stiamo bloccando perché la funzione restituisce il controllo al programma principale.
Quindi, quando la promessa si risolve, stiamo usando il generatore per restituire il controllo alla funzione asincrona con il valore della promessa risolta.
Puoi leggere di più qui per un'ottima panoramica dei generatori e del codice asincrono.
In effetti, ora possiamo scrivere codice asincrono che assomiglia a codice sincrono. Ciò significa che è più facile ragionare e possiamo utilizzare strumenti sincroni per la gestione degli errori come try/catch:
const getData = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1")
const data = await response.json()
console.log(data)
} catch (err) {
console.log(err)
}
}
getData()
Bene. Allora come lo usiamo? Per utilizzare async / await dobbiamo anteporre async alla funzione. Questo non la rende una funzione asincrona, ma ci permette semplicemente di usare await al suo interno.
La mancata scrittura della parola chiave async comporterà un errore di sintassi quando si tenterà di utilizzare await all'interno di una normale funzione.
const getData = async () => {
console.log("Possiamo usare await in questa funzione")
}
Per questo motivo, non possiamo utilizzare async / await sul codice di primo livello. Ma async e await sono ancora solo un fronzolo sintattico rispetto alle promesse. Quindi possiamo gestire casi di alto livello con il concatenamento delle promesse:
async function getData() {
let response = await fetch('http://apiurl.com');
}
// getData is a promise
getData().then(res => console.log(res)).catch(err => console.log(err);
Questo evidenzia un altro fatto interessante su async / await. Quando si definisce una funzione come asincrona, restituirà sempre una promessa.
L'uso di async / await può sembrare una magia all'inizio. Ma come ogni magia, è solo una tecnologia sufficientemente avanzata che si è evoluta nel corso degli anni. Si spera che ora tu abbia una solida conoscenza delle basi e che tu possa usare async / await con sicurezza.
Conclusione
Se sei arrivato qui, congratulazioni. Hai appena aggiunto alla tua cassetta degli attrezzi una conoscenza chiave su JavaScript e su come funziona con i suoi ambienti.
Questo è sicuramente un argomento confuso e le linee non sono sempre chiare. Ma ora si spera che tu abbia una miglior comprensione di come funziona JavaScript con il codice asincrono nel browser e una comprensione più forte sia delle promesse che di async / await.
Se ti è piaciuto questo articolo, ti potrebbe piacere anche il mio canale youtube (in inglese). Al momento ho una serie di nozioni di base sul Web che tratta argomenti come HTTP , costruire un server Web da zero e altro ancora.
C'è anche una serie sulla costruzione di un'intera app con React , se ti può interessare. E ho intenzione di aggiungere molti più contenuti qui in futuro andando in profondità sugli argomenti JavaScript.
E se vuoi salutarmi o chattare con me sullo sviluppo web, puoi sempre contattarmi su Twitter a @foseberg. Grazie per aver letto!