Programmazione Asincrona JavaScript e Callback
Asincronia 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 consumer attuali, ogni programma viene eseguito per uno slot di tempo specifico e poi interrompe la sua esecuzione per permettere ad un altro programma di continuare la propria. Questa cosa gira in un ciclo così veloce che è impossibile notarlo. Pensiamo che i nostri computer eseguano molti programmi simultaneamente, ma questa è un'illusione (eccetto sulle macchine multiprocessore).
I programmi internamente usano le interruzioni, un segnale emesso al processore per attirare l'attenzione del sistema.
Non entriamo nei dettagli interni di questo ora, ma teniamo presente che è normale che i programmi siano asincroni e interrompano la loro esecuzione fino a quando non hanno bisogno di attenzione, permettendo al computer di eseguire altre cose nel frattempo. Quando un programma sta aspettando una risposta dalla rete, non può bloccare il processore fino a quando la richiesta non termina.
Normalmente, i linguaggi di programmazione sono sincroni e alcuni forniscono un modo per gestire l'asincronia nel linguaggio o tramite librerie. C, Java, C#, PHP, Go, Ruby, Swift e Python sono tutti sincroni di default. Alcuni di essi gestiscono le operazioni asincrone usando thread, generando un nuovo processo.
JavaScript
JavaScript è sincrono di default ed è single-threaded. Questo significa che il codice non può creare nuovi thread ed eseguire 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 lavoro principale, all'inizio, era quello di rispondere alle azioni dell'utente, come onClick
, onMouseOver
, onChange
, onSubmit
e così via. Come poteva fare questo con un modello di programmazione sincrono?
La risposta era nel suo ambiente. Il browser fornisce un modo per farlo fornendo un set di API che possono gestire questo tipo di funzionalità.
Più recentemente, Node.js ha introdotto un ambiente I/O non bloccante per estendere questo concetto all'accesso ai file, alle chiamate di rete e così via.
Callback
Non si può sapere quando un utente cliccherà un pulsante. Quindi, si definisce un gestore eventi per l'evento click. Questo gestore eventi accetta una funzione, che verrà chiamata quando l'evento viene attivato:
document.getElementById('button').addEventListener('click', () => {
// elemento cliccato
});
Questa è la cosiddetta callback.
Una callback è una semplice funzione passata come valore a un'altra funzione e verrà eseguita solo quando si verifica l'evento. Possiamo fare questo perché JavaScript ha funzioni di prima classe, che possono essere assegnate a variabili e passate ad altre funzioni (chiamate funzioni di ordine superiore)
È comune racchiudere tutto il codice client in un listener di eventi load sull'oggetto window, che esegue la funzione di callback solo quando la pagina è pronta:
window.addEventListener('load', () => {
// finestra caricata
// fai quello che vuoi
});
Le callback sono usate ovunque, non solo negli eventi DOM.
Un esempio comune è l'utilizzo di timer:
setTimeout(() => {
// viene eseguito dopo 2 secondi
}, 2000);
Le richieste XHR accettano anche una 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://yoursite.com');
xhr.send();
Gestione degli errori nelle callback
Come si gestiscono gli errori con le callback? Una strategia molto comune è quella utilizzata da Node.js: il primo parametro in qualsiasi funzione di callback è l'oggetto errore: callback con errore per primo
Se non c'è alcun errore, l'oggetto è null. Se c'è 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 dei callback
I callback sono ottimi per casi semplici!
Tuttavia, ogni callback aggiunge un livello di annidamento, e quando si hanno molti callback, il codice inizia a complicarsi molto rapidamente:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
});
}, 2000);
});
});
Questo è solo un codice semplice a 4 livelli, ma ne ho visti con molti più livelli di annidamento e non è divertente.
Come lo risolviamo?
Alternative ai callback
A partire da ES6, JavaScript ha introdotto diverse funzionalità che ci aiutano con il codice asincrono che non prevede l'utilizzo di callback: Promises
(ES6) e Async/Await
(ES2017).