20 marzo 2025

Comprendere il Ciclo degli Eventi, i Callback, le Promesse e l’Asincronicità

La programmazione asincrona è un concetto fondamentale in JavaScript e comprendere come funziona è essenziale per costruire applicazioni web efficienti e non bloccanti. In questo articolo del blog, esploreremo a fondo il ciclo degli eventi, i callback, le promesse, async/await e strumenti popolari come AJAX e l’API Fetch che aiutano a gestire il codice asincrono in JavaScript.

Ciclo degli Eventi: Il Cuore del Comportamento Asincrono di JavaScript

Il modello di esecuzione di JavaScript è costruito attorno a un ciclo degli eventi, che orchestra l’esecuzione del codice, gestisce gli eventi e processa i compiti in coda. Ma come funziona effettivamente questo processo dietro le quinte? Analizziamolo passo dopo passo.

Concetti di Esecuzione

Stack

In JavaScript, lo stack di chiamate tiene traccia delle chiamate delle funzioni. Quando una funzione viene invocata, viene creato un frame dello stack per memorizzare i riferimenti agli argomenti e alle variabili locali della funzione. Il frame dello stack viene rimosso quando la funzione restituisce un valore.

Ecco un esempio:

function foo(b) {
  const a = 10;
  return a + b + 11;
}

function bar(x) {
  const y = 3;
  return foo(x * y);
}

const baz = bar(7); // assigns 42 to baz

Heap

Gli oggetti di JavaScript vengono allocati in aree di memoria conosciute come heap. L’heap è una grande area di memoria non strutturata in cui gli oggetti vengono memorizzati dinamicamente durante l’esecuzione del programma.

Coda

La coda dei messaggi di JavaScript tiene traccia degli eventi e dei compiti da elaborare. Quando si verifica un evento (come un clic dell’utente), un messaggio viene aggiunto alla coda. Il ciclo degli eventi preleva questi messaggi e chiama le relative funzioni quando lo stack è vuoto.

Il Ciclo degli Eventi in Azione

Il ciclo degli eventi opera in un ciclo continuo, elaborando i messaggi in coda uno alla volta:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

Questo assicura che ogni messaggio venga elaborato completamente prima di passare al successivo. Il modello “run-to-completion” di JavaScript significa che una volta che una funzione inizia, essa verrà eseguita completamente prima che qualsiasi altro codice possa essere eseguito.

Tuttavia, i compiti che richiedono molto tempo possono bloccare l’interfaccia utente, impedendo l’elaborazione delle azioni dell’utente come clic o scroll. Per mitigare questo, è una buona pratica suddividere i compiti lunghi in parti più piccole o utilizzare tecniche asincrone.

Aggiungere Messaggi alla Coda

Eventi, come interazioni dell’utente (clic, pressione dei tasti), vengono generalmente aggiunti alla coda. Ad esempio, la funzione setTimeout() consente di aggiungere un messaggio ritardato alla coda, che verrà eseguito dopo un ritardo minimo.

Thread di Lavoro

Nei casi in cui è necessaria una computazione pesante, i web workers forniscono un modo per eseguire codice in un thread separato. Essi comunicano con il thread principale tramite il metodo postMessage, il che garantisce che il thread dell’interfaccia utente rimanga non bloccato.

Callback: La Base della Programmazione Asincrona in JavaScript

Una callback è una funzione passata come argomento a un’altra funzione, che viene invocata in un momento successivo. Le callback sono ampiamente utilizzate nella programmazione asincrona per gestire operazioni che richiedono tempo, come il recupero dei dati.

Esistono due tipi di callback:

Una sfida che si presenta con le callback è il Callback Hell (noto anche come la Piramide della Doom), dove molte callback annidate rendono il codice difficile da leggere e mantenere.

Il Livello Successivo nella Gestione Asincrona

Le Promise sono un modo più pulito e strutturato per gestire le operazioni asincrone in JavaScript. Una promise rappresenta un’operazione che sarà completata in futuro e che può avere successo (completata) o fallire (rifiutata).

Una promise si trova in uno dei seguenti tre stati:

Puoi concatenare operazioni su una promise utilizzando i metodi .then(), .catch(), e .finally() per gestire rispettivamente il successo, l’errore e la pulizia.

const fetchData = fetch("https://api.example.com/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

Catena di Promesse

Con la catena di promesse, puoi gestire una sequenza di operazioni asincrone senza dover ricorrere a callback annidati. Ogni metodo .then() restituisce una nuova promise, che può essere gestita con un altro .then() o .catch().

Promesse in Catena

Collegando insieme le promesse, puoi gestire flussi di lavoro asincroni complessi. Ogni promise restituisce una nuova promise e, a seconda dello stato della promise originale (completata o rifiutata), verrà invocato il callback appropriato.

Thenables

I Thenables sono oggetti che implementano il metodo .then(). Le promesse sono esempi di thenables, ma puoi anche creare oggetti personalizzati che seguono questo schema e si comportano come promesse.

const aThenable = {
  then(onFulfilled, onRejected) {
    onFulfilled({
      then(onFulfilled, onRejected) {
        onFulfilled(42);
      },
    });
  },
};

Promise.resolve(aThenable).then((result) => console.log(result)); // 42

Concorrenza delle Promesse

JavaScript offre diversi modi per gestire la concorrenza con le promesse:

Questi metodi ti permettono di gestire più promesse contemporaneamente, migliorando l’efficienza e rendendo il codice più leggibile.

Async/Await (ES8): Un Approccio Più Semplice

Async/await è stato introdotto in ES8 per semplificare e rendere più chiaro il lavoro con il codice asincrono. Una funzione async restituisce sempre una promessa, e all’interno della funzione puoi usare await per sospendere l’esecuzione fino a quando una promessa non viene risolta o rifiutata.

async function fetchData() {
  const response = await fetch("https://api.example.com/data");
  const data = await response.json();
  return data;
}

Con async/await, il codice asincrono appare più simile a quello sincrono, rendendolo più facile da leggere e gestire.

AJAX e Fetch API: Gestire le Richieste HTTP

AJAX (Utilizzando XMLHttpRequest)

AJAX (JavaScript Asincrono e XML) è una tecnica che consente di inviare richieste HTTP in modo asincrono dal browser. Tuttavia, è piuttosto verboso e basato su callback, il che può portare a un codice complesso e difficile da mantenere.

const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data", true);

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(JSON.parse(xhr.responseText)); // Handle the response
  }
};

xhr.send();

Fetch API: Un Approccio Moderno

La Fetch API è un’alternativa più moderna e basata su promesse a AJAX. Semplifica la sintassi e rende la gestione delle richieste HTTP asincrone molto più facile da gestire.

fetch("https://api.example.com/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

Con la Fetch API, puoi gestire facilmente le richieste asincrone, analizzare le risposte e gestire gli errori in modo più pulito e intuitivo.