JavaScript e gli eventi: controllare il ciclo di vita degli ascoltatori con AbortController

by Anastasia P.
SHARE
JavaScript e gli eventi: controllare il ciclo di vita degli ascoltatori con AbortController
© Guida-HTML5.it

Introduzione

Nel modello a eventi di JavaScript, un elemento può reagire a un’azione dell’utente o a un evento generato dal browser, come un clic, una pressione di un tasto o la modifica della pagina. Solitamente utilizziamo addEventListener() per collegare una funzione a un evento e lasciamo che il browser la esegua quando necessario.

In applicazioni semplici, aggiungere un ascoltatore è sufficiente. In applicazioni più grandi, però, è altrettanto importante sapere quando smettere di ascoltare. Un componente può essere rimosso dalla pagina, una finestra modale può essere chiusa oppure una richiesta può diventare irrilevante. Se gli ascoltatori rimangono attivi, possono eseguire codice non più necessario e rendere il comportamento dell’applicazione più difficile da controllare.

JavaScript offre due approcci principali: rimuovere manualmente gli ascoltatori con removeEventListener() oppure associarli a un AbortController. Il secondo approccio permette di interrompere in modo centralizzato uno o più ascoltatori tramite un segnale condiviso.

Codice completo

<button id="apri-pannello">Apri pannello</button>

<section id="pannello" hidden>
  <h2>Pannello interattivo</h2>
  <p>Eventi attivi: <strong id="contatore">0</strong></p>
  <button id="chiudi-pannello">Chiudi pannello</button>
</section>

<script>
  const pulsanteApri = document.querySelector("#apri-pannello");
  const pannello = document.querySelector("#pannello");
  const pulsanteChiudi = document.querySelector("#chiudi-pannello");
  const contatore = document.querySelector("#contatore");

  let controllerCorrente = null;
  let numeroEventi = 0;

  function aggiornaContatore() {
    numeroEventi += 1;
    contatore.textContent = numeroEventi;
  }

  function attivaEventiDelPannello() {
    // Se esiste un controller precedente, interrompiamo
    // gli eventuali ascoltatori ancora registrati.
    controllerCorrente?.abort();

    const controller = new AbortController();
    controllerCorrente = controller;

    // Tutti gli ascoltatori usano lo stesso segnale.
    window.addEventListener("keydown", (evento) => {
      if (evento.key === "Escape") {
        chiudiPannello();
      }
    }, {
      signal: controller.signal
    });

    window.addEventListener("resize", aggiornaContatore, {
      signal: controller.signal
    });

    pulsanteChiudi.addEventListener("click", chiudiPannello, {
      signal: controller.signal
    });
  }

  function apriPannello() {
    pannello.hidden = false;
    pulsanteApri.disabled = true;
    numeroEventi = 0;
    contatore.textContent = numeroEventi;

    attivaEventiDelPannello();
    pulsanteChiudi.focus();
  }

  function chiudiPannello() {
    pannello.hidden = true;
    pulsanteApri.disabled = false;

    // Rimuove in un´unica operazione tutti gli ascoltatori
    // collegati a questo pannello.
    controllerCorrente?.abort();
    controllerCorrente = null;

    pulsanteApri.focus();
  }

  pulsanteApri.addEventListener("click", apriPannello);
</script>

Spiegazione

Che cos’è AbortController?

AbortController è un oggetto progettato per inviare un segnale di interruzione. Il segnale si trova nella proprietà signal, mentre il metodo abort() comunica a tutte le operazioni collegate che devono essere terminate.

Quando si registra un ascoltatore, si può passare il segnale all’interno delle opzioni di addEventListener():

const controller = new AbortController();

elemento.addEventListener("click", gestisciClick, {
  signal: controller.signal
});

// L´ascoltatore viene rimosso automaticamente.
controller.abort();

Dopo la chiamata a abort(), l’ascoltatore non viene più eseguito. Questo è particolarmente utile quando più eventi appartengono allo stesso componente. Nel codice completo, il pannello ascolta il clic sul pulsante di chiusura, il tasto Escape e il ridimensionamento della finestra. Tutti questi ascoltatori condividono lo stesso AbortSignal.

Perché usare un controller condiviso?

Senza un controller, sarebbe necessario conservare ogni funzione e rimuovere gli ascoltatori uno alla volta:

window.removeEventListener("resize", aggiornaContatore);
pulsanteChiudi.removeEventListener("click", chiudiPannello);
window.removeEventListener("keydown", gestisciTasto);

Questa tecnica funziona, ma richiede attenzione. La funzione passata a removeEventListener() deve essere la stessa istanza usata durante la registrazione. Non è corretto creare una nuova funzione anonima per tentare di rimuovere quella precedente.

Con un controller, invece, il ciclo di vita è più chiaro: quando il componente viene aperto, gli eventi vengono attivati; quando viene chiuso, il controller interrompe tutti gli ascoltatori collegati.

Gestire più aperture dello stesso componente

Nel metodo attivaEventiDelPannello() viene eseguito prima:

controllerCorrente?.abort();

L’operatore ?. evita un errore quando il controller non esiste ancora. Questa riga protegge dal problema degli ascoltatori duplicati. Se il pannello venisse aperto più volte senza interrompere il controller precedente, un singolo evento potrebbe attivare la stessa logica diverse volte.

Questo schema è utile per finestre modali, menu laterali, componenti di una single-page application e interfacce che vengono create e distrutte dinamicamente.

AbortController e richieste fetch

Lo stesso controller può interrompere anche una richiesta fetch(). In questo modo eventi e operazioni asincrone possono seguire lo stesso ciclo di vita:

const controller = new AbortController();

fetch("/api/profilo", {
  signal: controller.signal
})
  .then((risposta) => risposta.json())
  .then((profilo) => {
    console.log(profilo);
  })
  .catch((errore) => {
    if (errore.name === "AbortError") {
      console.log("Richiesta interrotta intenzionalmente");
      return;
    }

    console.error("Errore di rete:", errore);
  });

// Per annullare la richiesta:
controller.abort();

Quando un componente viene smontato, lo stesso controller può quindi interrompere sia gli ascoltatori sia le richieste ancora in corso.

Best practice

  • Definisci un proprietario degli eventi: ogni componente dovrebbe sapere quali ascoltatori gli appartengono e quando devono essere rimossi.
  • Usa un controller per gruppo logico: non è necessario creare un controller globale per tutta l’applicazione. È preferibile crearne uno per modale, pagina o funzionalità.
  • Evita ascoltatori duplicati: se una funzione può essere chiamata più volte, interrompi il gruppo precedente prima di crearne uno nuovo.
  • Gestisci l’annullamento esplicitamente: quando usi fetch(), controlla se l’errore è un AbortError, distinguendolo da un vero errore di rete.
  • Conserva il controller corrente: una variabile come controllerCorrente permette di interrompere il componente anche da una funzione diversa da quella che lo ha inizializzato.
  • Usa removeEventListener quando serve precisione: per un singolo ascoltatore stabile e indipendente, la rimozione manuale può essere più semplice.
  • Non abusare degli ascoltatori globali: eventi su window e document sono potenti, ma devono essere rimossi quando il componente non è più visibile o utilizzabile.

Riepilogo

Il modello a eventi non riguarda soltanto la registrazione delle funzioni, ma anche la loro durata. Un ascoltatore deve essere attivo solo finché il componente che lo utilizza è presente e necessario.

AbortController offre un modo ordinato per controllare questo ciclo di vita. Associando più ascoltatori allo stesso AbortSignal, è possibile rimuoverli tutti con una singola chiamata a abort(). Il risultato è codice più facile da mantenere, meno soggetto a duplicazioni e più adatto ad applicazioni dinamiche.

La tecnica diventa ancora più utile quando viene combinata con fetch(): un unico controller può interrompere eventi e attività asincrone appartenenti allo stesso componente.

Approfondisci con risorse ufficiali

SHARE