Frontend

Sincronia e asincronia in JavaScript

Autore

Manuel Ricci

Nel contesto della programmazione, la sincronia implica che le operazioni di codice vengono eseguite una dopo l'altra. Ogni istruzione deve attendere il completamento della precedente prima di iniziare. Questo approccio è semplice da capire ma può portare a inefficienze, specialmente quando un'operazione richiede tempo per essere completata, come una richiesta di rete o l'accesso a un database.

1console.log("Inizio");
2let valore = funzioneSincrona(); // Questa funzione potrebbe richiedere tempo
3console.log(valore);
4console.log("Fine");

Nell'asincronia, alcune operazioni possono essere messe in coda e eseguite in seguito, permettendo al programma di continuare l'esecuzione di altre istruzioni. Ciò è particolarmente utile in scenari come il web, dove le richieste di rete e le operazioni su database possono impiegare un tempo imprevedibile. La programmazione asincrona aiuta a mantenere le applicazioni reattive e efficienti.

1console.log("Inizio");
2funzioneAsincrona(valore => {
3   console.log(valore); // Questo viene eseguito dopo che funzioneAsincrona è completa
4});
5console.log("Questa linea viene eseguita mentre funzioneAsincrona è ancora in esecuzione");

Perché l’asincronia è importante nello sviluppo web moderno

Nel web moderno, gli utenti si aspettano applicazioni veloci e reattive. L'asincronia permette agli sviluppatori di creare interfacce utente che non si bloccano durante operazioni prolungate come caricamenti di dati o elaborazioni.

Le operazioni di rete sono intrinsecamente asincrone. L'uso di pattern asincroni in JavaScript è fondamentale per gestire le richieste HTTP, come il recupero di dati da un server, in modo efficiente.

Anche se JavaScript ha un singolo thread di esecuzione, il modello di eventi asincroni consente di gestire numerose operazioni in modo concorrente, migliorando così le prestazioni complessive delle applicazioni.

L'asincronia aiuta nella creazione di applicazioni che possono facilmente scalare. Gestire correttamente le operazioni asincrone significa poter servire un numero elevato di utenti senza degradare le prestazioni dell'applicazione.

Concetti di Base del JavaScript Asincrono

Callback

  • Definizione: Un callback è una funzione passata come argomento ad un'altra funzione, che verrà poi eseguita all'interno di quest'ultima. I callback sono il modo tradizionale di gestire operazioni asincrone in JavaScript.

    Esempio di Callback: ```js function operazioneAsincrona(callback) { setTimeout(() => { callback('Risultato'); }, 1000); }

    operazioneAsincrona(risultato => { console.log(risultato); // Verrà stampato "Risultato" dopo 1 secondo }); ```

  • Problemi comuni: Sebbene i callback siano semplici, possono portare a complicazioni come il "Callback Hell" o "Pyramid of Doom", dove si ha una serie di callback annidati che rendono il codice difficile da leggere e mantenere.

Promise

  • Definizione: Una promise in JavaScript rappresenta il completamento (o il fallimento) di un'operazione asincrona e il suo valore risultante. È un oggetto che può essere in uno dei tre stati: pending, fulfilled, o rejected.

    Esempio di promise: ```js function operazioneAsincrona() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('Completato'); }, 1000); }); }

    operazioneAsincrona() .then(risultato => console.log(risultato)) // "Completato" dopo 1 secondo .catch(errore => console.error(errore)); ```

  • Metodi .then(), .catch(), e .finally(): Le promesse utilizzano questi metodi per gestire il successo, l'errore e la conclusione della promessa.

Async/Await

  • Definizione: async e await sono estensioni del modello di promise che semplificano la scrittura di codice asincrono rendendolo più simile a quello sincrono.

    Esempio di Async/Await: ```js async function operazioneAsincrona() { return 'Completato'; }

    async function esegui() { try { let risultato = await operazioneAsincrona(); console.log(risultato); // "Completato" } catch (errore) { console.error(errore); } }

    esegui(); ```

async e await rendono il codice più leggibile e facile da seguire, in particolare quando si tratta di più operazioni asincrone in serie o in parallelo.

Callback in JavaScript

Un callback è una funzione che viene passata come argomento ad un'altra funzione e viene eseguita dopo che un'operazione asincrona è stata completata. I callback sono fondamentali nella gestione delle operazioni asincrone in JavaScript, soprattutto in scenari come richieste di rete, operazioni su file, o timeout.

Esempio di Callback: ```js function leggiFile(nomeFile, callback) { // Simula la lettura di un file setTimeout(() => { const risultato = 'Contenuto del file ' + nomeFile; callback(risultato); }, 1000); }

leggiFile('mioFile.txt', contenuto => { console.log(contenuto); // Stampa il contenuto dopo 1 secondo }); ```

Problemi comuni con i callback

  • Callback Hell: Quando si utilizzano molteplici operazioni asincrone annidate, i callback possono portare a una struttura di codice complessa e difficile da leggere, spesso chiamata "Callback Hell" o "Pyramid of Doom". Questo problema si verifica quando i callback vengono annidati in profondità, rendendo il codice poco chiaro e difficile da mantenere.

    Esempio di Callback Hell: js leggiFile('file1.txt', contenuto1 => { console.log(contenuto1); leggiFile('file2.txt', contenuto2 => { console.log(contenuto2); leggiFile('file3.txt', contenuto3 => { console.log(contenuto3); // Continua l'annidamento... }); }); });

Tecniche per Evitare i Problemi dei Callback

  • Modularizzazione: Suddividere il codice in funzioni più piccole e riutilizzabili può aiutare a ridurre la complessità e a migliorare la leggibilità.
  • Promise e Async/Await: Convertire i callback in promise e utilizzare async/await può semplificare notevolmente la gestione delle operazioni asincrone, riducendo l'annidamento e migliorando la leggibilità del codice.
  • Librerie di Utility: Esistono diverse librerie, come Bluebird o Async.js, che offrono funzioni utili per gestire i callback in modo più ordinato e funzionale.

Promesse in JavaScript

Una promessa è un oggetto che rappresenta il risultato finale di un'operazione asincrona.

Una promessa può trovarsi in uno dei tre stati:

  • Pending: lo stato iniziale, né soddisfatta né respinta.
  • Fulfilled: significa che l'operazione è stata completata con successo.
  • Rejected: significa che l'operazione ha fallito.

    Esempio di Creazione di una Promessa: js let promessa = new Promise((resolve, reject) => { // Simula un'operazione asincrona setTimeout(() => { let successo = true; // Modifica questa variabile per testare il reject if (successo) { resolve('Operazione completata con successo'); } else { reject('Errore nell'operazione'); } }, 1000); });

Metodi .then(), .catch(), e .finally()

  • .then(): Viene utilizzato per gestire il risultato di una promessa soddisfatta.
  • .catch(): Serve per gestire l'errore nel caso in cui la promessa sia respinta.
  • .finally(): Viene eseguito sempre, indipendentemente dal risultato della promessa, utile per eseguire la pulizia del codice.

    Esempio di Utilizzo di .then(), .catch() e .finally(): js promessa .then(risultato => { console.log(risultato); // Gestisce il caso di successo }) .catch(errore => { console.error(errore); // Gestisce l'errore }) .finally(() => { console.log('Promessa eseguita, con successo o meno.'); // Si esegue sempre });

Catena di Promesse e Composizione

  • Catena di Promesse: Le promesse possono essere collegate insieme per creare sequenze di operazioni asincrone.
  • Composizione: Utilizzando .then(), è possibile gestire il risultato di una promessa e restituire un'altra promessa, creando così una catena.

    Esempio di Catena di Promesse: ```js function primaOperazione() { return new Promise(resolve => setTimeout(() => resolve('Risultato 1'), 1000)); }

    function secondaOperazione(risultato1) { return new Promise(resolve => setTimeout(() => resolve(risultato1 + ' e Risultato 2'), 1000)); }

    primaOperazione() .then(risultato1 => secondaOperazione(risultato1)) .then(risultatoFinale => console.log(risultatoFinale)); // Stampa "Risultato 1 e Risultato 2" ```

Error Handling

  • Gestione degli Errori: È cruciale gestire correttamente gli errori nelle promesse. Se un errore avviene in una promessa e non viene catturato, può portare a comportamenti imprevisti o al fallimento silenzioso del codice.

    Esempio di Gestione degli Errori: js new Promise((resolve, reject) => { throw new Error('Errore nella promessa'); }) .catch(errore => { console.error(errore.message); // Gestisce l'errore lanciato nella promessa });

Async/Await in JavaScript

Introdotte in ES2017, async e await sono due parole chiave in JavaScript che permettono di scrivere codice asincrono in modo più leggibile, simile a quello sincrono. async rende una funzione asincrona, restituendo una promessa, mentre await può essere usato all'interno di funzioni async per aspettare il completamento di una promessa.

Utilizzo di Async/Await

Dichiarando una funzione con async, automaticamente questa restituirà una promessa.

Esempio di Funzione Async: ```js async function operazioneAsincrona() { return 'Completato'; }

operazioneAsincrona().then(risultato => console.log(risultato)); // "Completato" ```

  • Await: await viene utilizzato per aspettare che una promessa si risolva. Blocca l'esecuzione della funzione async corrente, in un modo non bloccante per il resto dell'applicazione, fino a quando la promessa non si risolve.

    Esempio di Utilizzo di Await: ```js async function esempioAwait() { let valore = await operazioneAsincrona(); console.log(valore); // "Completato" }

    esempioAwait(); ```

Gestione degli Errori

In combinazione con async e await, try/catch può essere utilizzato per gestire gli errori in maniera elegante. Se una promessa viene respinta, l'errore può essere catturato e gestito all'interno del blocco catch.

Esempio di Gestione degli Errori con Try/Catch: ```js async function esempioErrore() { try { let risultato = await operazioneAsincronaChePotrebbeFallire(); console.log(risultato); } catch (errore) { console.error('Si è verificato un errore:', errore); } }

esempioErrore(); ```

Vantaggi di Async/Await

  • Leggibilità e Manutenibilità: Il codice scritto con async e await è più facile da leggere e mantenere rispetto ai callback tradizionali o alle catene di promesse.
  • Flusso di Controllo Chiaro: Il codice asincrono scritto con async e await assomiglia a quello sincrono, rendendo più semplice la comprensione del flusso di controllo.
  • Migliore Gestione degli Errori: La sintassi try/catch permette una gestione degli errori più naturale e familiare.

Patterns avanzati nel JavaScript Asincrono

Parallelismo: Eseguire più operazioni asincrone in parallelo significa avviarle contemporaneamente, senza aspettare che ciascuna si completi prima di iniziare la successiva. È utile quando le operazioni sono indipendenti l'una dall'altra.

Esempio di Parallelismo: ```js let promessa1 = operazioneAsincrona1(); let promessa2 = operazioneAsincrona2();

Promise.all([promessa1, promessa2]) .then(([risultato1, risultato2]) => { console.log(risultato1, risultato2); // Risultati di entrambe le operazioni }); ```

Serializzazione: Significa eseguire operazioni asincrone una dopo l'altra, iniziando una solo dopo il completamento della precedente. È necessario quando l'output di un'operazione dipende dal risultato di un'altra.

Esempio di Serializzazione: js operazioneAsincrona1() .then(risultato1 => { return operazioneAsincrona2(risultato1); }) .then(risultato2 => { console.log(risultato2); // Risultato della seconda operazione });

Utilizzo di Promise.all() e Promise.race()

Promise.all(): Questo metodo viene utilizzato per eseguire più promesse in parallelo e attendere che tutte si risolvano. Restituisce una singola promessa che si risolve quando tutte le promesse nell'iterabile passato si sono risolte o se uno dei promise è respinto.

Esempio di Promise.all(): js Promise.all([operazioneAsincrona1(), operazioneAsincrona2()]) .then(([risultato1, risultato2]) => { console.log(risultato1, risultato2); // Risultati di entrambe le operazioni });

Promise.race(): Questo metodo restituisce una promessa che si risolve o rifiuta non appena una delle promesse nell'iterabile si risolve o rifiuta, con il valore o il motivo di quella promessa.

Esempio di Promise.race(): js Promise.race([operazioneAsincrona1(), operazioneAsincrona2()]) .then(risultato => { console.log(risultato); // Risultato della prima promessa completata });

Esempi Pratici di Utilizzo in Scenari Reali

  • Caricamento di Risorse in Parallelo: Utilizzo di Promise.all() per caricare contemporaneamente più risorse da diverse API.
  • Timeout per Richieste di Rete: Uso di Promise.race() per implementare un timeout su una richiesta di rete, competendo la richiesta di rete con una promessa che si rifiuta automaticamente dopo un certo periodo.

Fetch API e AJAX nel JavaScript Asincrono

La Fetch API è una moderna interfaccia JavaScript per operazioni HTTP. Fornisce un modo più potente e flessibile per fare richieste di rete rispetto a XMLHttpRequest (AJAX). La Fetch API utilizza promesse, rendendola una scelta naturale per operazioni asincrone.

Esempio di Utilizzo della Fetch API: js fetch('https://api.esempio.com/dati') .then(response => response.json()) .then(dati => console.log(dati)) .catch(errore => console.error('Errore:', errore));

La Fetch API fornisce una risposta sotto forma di promessa che deve essere processata per ottenere il contenuto effettivo. La risposta può essere convertita in vari formati, come JSON, testo o blob.

AJAX con XMLHttpRequest

AJAX (Asynchronous JavaScript and XML) è una tecnica per creare applicazioni web interattive e reattive. Utilizza l'oggetto XMLHttpRequest per effettuare richieste di rete asincrone.

Esempio di Utilizzo di AJAX: js let xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.esempio.com/dati', true); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { console.log(JSON.parse(xhr.responseText)); } }; xhr.send();

Mentre XMLHttpRequest è più vecchio e supportato da tutti i browser, la Fetch API offre una sintassi più pulita e basata su promesse. Tuttavia, AJAX con XMLHttpRequest è ancora ampiamente utilizzato per la compatibilità con browser più vecchi.

Gestione delle Risposte e degli Errori di Rete

  • Gestione delle Risposte: Dopo aver effettuato una richiesta di rete, è fondamentale gestire correttamente la risposta, sia in caso di successo sia in caso di errore.
  • Errori di Rete: Gli errori di rete possono essere dovuti a problemi di connessione, timeout, risposte di errore dal server, ecc. È importante gestire questi errori in modo elegante per garantire una buona esperienza utente.

    Esempio di Gestione degli Errori: js fetch('https://api.esempio.com/dati') .then(response => { if (!response.ok) { throw new Error('Network response was not ok ' + response.statusText); } return response.json(); }) .then(dati => console.log(dati)) .catch(errore => console.error('Errore di rete:', errore));

Event Loop e Concorrenza in JavaScript

L'Event Loop è un meccanismo che permette a JavaScript, che è un linguaggio a singolo thread, di eseguire operazioni asincrone (come I/O, eventi, ecc.) senza bloccare il thread principale.

Il funzionamento è riassumibile in due punti:

  1. Task Queue: Quando un'operazione asincrona viene completata (ad esempio, una risposta HTTP arriva), un evento o un callback associato viene aggiunto alla coda di task (o coda di messaggi).
  2. Loop: L'Event Loop controlla continuamente la coda e, quando il call stack è vuoto, prende il primo evento/callback dalla coda e lo esegue.

Questo meccanismo permette a JavaScript di gestire molteplici operazioni asincrone, eseguendo i loro callback uno alla volta, pur rimanendo un linguaggio a singolo thread.

Esempio Concettuale dell'Event Loop: ```js console.log('Primo');

setTimeout(() => { console.log('Secondo'); }, 0);

console.log('Terzo');

// Output: // Primo // Terzo // Secondo `` In questo esempio, anche se ilsetTimeout` ha un ritardo di 0 millisecondi, il suo callback viene eseguito dopo perché passa attraverso l'Event Loop.

Concorrenza

JavaScript utilizza un modello di concorrenza basato sull'Event Loop. Questo modello consente di eseguire operazioni asincrone in maniera efficiente e non bloccante, sfruttando callback, promesse e async/await.

Infine, per gestire operazioni più pesanti senza bloccare l'interfaccia utente, si possono utilizzare i Web Workers. I Web Workers permettono di eseguire codice JavaScript in background, su thread separati dal thread principale.

Esempio di Utilizzo dei Web Workers: js let worker = new Worker('workerScript.js'); worker.onmessage = function(event) { console.log('Risposta dal worker:', event.data); }; worker.postMessage('Hello World'); In questo esempio, un Web Worker esegue lo script 'workerScript.js' e comunica con il thread principale tramite messaggi.

Caricamento...

Diventiamo amici di penna? ✒️

Iscriviti alla newsletter per ricevere una mail ogni paio di settimane con le ultime novità, gli ultimi approfondimenti e gli ultimi corsi gratuiti puubblicati. Ogni tanto potrei scrivere qualcosa di commerciale, ma solo se mi autorizzi, altrimenti non ti disturberò oltre.

Se non ti piace la newsletter ti ricordo la community su Discord, dove puoi chiedere aiuto, fare domande e condividere le tue esperienze (ma soprattutto scambiare due meme con me). Ti aspetto!

Ho in previsione di mandarti una newsletter ogni due settimane e una commerciale quando capita.