Programmazione asincrona in JavaScript e Callback
Asincronicità nei Linguaggi di Programmazione
I computer sono asincroni per progettazione.
Asincrono significa che le cose possono accadere indipendentemente dal flusso principale del programma.
Nei computer per consumatori attuali, ogni programma viene eseguito per un determinato intervallo di tempo e poi interrompe la sua esecuzione per consentire a un altro programma di continuare la sua esecuzione. Questa cosa si verifica in un ciclo così veloce che è impossibile notarla. Pensiamo che i nostri computer eseguano molti programmi contemporaneamente, ma questa è un’illusione (tranne che su macchine multiprocessore).
I programmi utilizzano internamente gli interrupt, un segnale emesso al processore per attirare l’attenzione del sistema.
Non entriamo nei dettagli di questo argomento ora, ma tieni presente che è normale che i programmi siano asincroni e interrompano la loro esecuzione fino a quando non hanno bisogno di attenzione, consentendo al computer di eseguire altre attività nel frattempo. Quando un programma attende una risposta dalla rete, non può interrompere il processore fino a quando la richiesta non è completata.
Normalmente, i linguaggi di programmazione sono sincroni e alcuni forniscono un modo per gestire l’asincronicità nel linguaggio o attraverso librerie. C, Java, C#, PHP, Go, Ruby, Swift e Python sono tutti sincroni per impostazione predefinita. Alcuni di essi gestiscono le operazioni asincrone utilizzando thread, avviando un nuovo processo.
JavaScript
JavaScript è sincrono per impostazione predefinita e è single threaded. Ciò significa che il codice non può creare nuovi thread e eseguirsi in parallelo.
Le righe di codice vengono eseguite in serie, una dopo l’altra, ad esempio:
const a = 1;const b = 2;const c = a * b;console.log(c);doSomething();
Ma JavaScript è nato all’interno del browser, il suo compito principale, all’inizio, era rispondere alle azioni dell’utente, come onClick
, onMouseOver
, onChange
, onSubmit
, ecc. Come poteva fare questo con un modello di programmazione sincrona?
La risposta era nel suo ambiente. Il browser fornisce un modo per farlo, fornendo un insieme di API in grado di gestire questo tipo di funzionalità .
Più recentemente, Node.js ha introdotto un ambiente di I/O non bloccante per estendere questo concetto all’accesso ai file, alle chiamate di rete e così via.
Callbacks
Non è possibile sapere quando un utente cliccherà un pulsante. Quindi, si definisce un gestore di eventi per l’evento di clic. Questo gestore di eventi accetta una funzione, che verrà chiamata quando si verifica l’evento:
document.getElementById("button").addEventListener("click", () => { // elemento cliccato});
Questo è il cosiddetto callback.
Un callback è una semplice funzione passata come valore a un’altra funzione e verrà eseguita solo quando si verifica l’evento. Possiamo farlo perché JavaScript ha funzioni di prima classe, che possono essere assegnate a variabili e passate ad altre funzioni (chiamate funzioni di ordine superiore).
È comune avvolgere tutto il codice del client in un listener di evento load
sull’oggetto window
, che esegue la funzione di callback solo quando la pagina è pronta:
window.addEventListener("load", () => { // finestra caricata // fai ciò che vuoi});
I callback sono utilizzati ovunque, non solo negli eventi del DOM.
Un esempio comune è quello di utilizzare i timer:
setTimeout(() => { // eseguito dopo 2 secondi}, 2000);
Le richieste XHR accettano anche un callback, in questo esempio assegnando una funzione a una proprietà che verrà chiamata quando si verifica un particolare evento (in questo caso, lo stato della richiesta cambia):
const xhr = new XMLHttpRequest();xhr.onreadystatechange = () => { if (xhr.readyState === 4) { xhr.status === 200 ? console.log(xhr.responseText) : console.error("errore"); }};xhr.open("GET", "https://iltuosito.com");xhr.send();
Gestione degli Errori nei Callbacks
Come gestisci gli errori con i callback? Una strategia molto comune è quella che Node.js ha adottato: il primo parametro in qualsiasi funzione di callback è l’oggetto di errore: error-first callbacks
Se non c’è alcun errore, l’oggetto è null
. Se si verifica un errore, contiene una descrizione dell’errore e altre informazioni.
const fs = require("node:fs");
fs.readFile("/file.json", (err, data) => { if (err) { // gestisci l'errore console.log(err); return; }
// nessun errore, elabora i dati console.log(data);});
Il problema con i Callbacks
I callback sono ottimi per i casi semplici!
Tuttavia, ogni callback aggiunge un livello di nidificazione e quando hai molti callback, il codice diventa complicato molto rapidamente:
window.addEventListener("load", () => { document.getElementById("button").addEventListener("click", () => { setTimeout(() => { items.forEach((item) => { // il tuo codice qui }); }, 2000); });});
Questo è solo un semplice codice con 4 livelli, ma ne ho visto molti di più e non è divertente.
Come risolviamo questo problema?
Alternative ai Callbacks
A partire da ES6, JavaScript ha introdotto diverse funzionalità che ci aiutano con il codice asincrono che non coinvolge l’uso di callback: Prom
ises (ES6) e Async/Await (ES2017).