Ge varje monterad JavaScript-vy en ny AbortController, registrera dess lyssnare med styrenhetens signal och avbryt den vid avmontering. Den här handledningen bygger en liten webbläsarvy och visar hur du kontrollerar upprepade monteringscykler i Chrome på en Mac.

Varför samlas händelsehanterare på hög när en vy öppnas igen?

En vy kan se ut att fungera som den ska första gången den öppnas, men börja reagera två gånger när den öppnas igen. En vanlig orsak är att initieringskoden skapar ytterligare en återanropsfunktion vid varje montering utan att ta bort den tidigare registreringen. Funktioner som skapas separat är olika lyssnarobjekt, även om källtexten är identisk. MDN:s dokumentation om addEventListener() förklarar denna regel för återanropsfunktioners identitet.

DOM förhindrar dubbla registreringar med samma händelsetyp, återanropsfunktion och capture-värde på samma mål. Det skyddet löser inte problemet med upprepad montering när varje initiering skapar en ny återanropsfunktion. Standarden anger också att en lyssnare tas bort när dess angivna signal avbryts. Dessa regler möjliggör en praktisk lösning: en styrenhet hanterar alla inbyggda händelselyssnare under en och samma monteringsperiod. DOM-standarden: EventTarget.

I den här handledningen ska läsaren öppna, stänga och öppna en räknarvy i vanlig JavaScript igen utan att lägga till extra knapp- eller fönsterhanterare. Exemplet använder syntetiska etiketter, kräver inget ramverk och bevarar räknarna mellan monteringarna.

Vilket livscykelkontrakt ska vyn följa?

Bestäm vad upprepade livscykelanrop ska innebära innan du implementerar initieringen. Följande kontrakt gör oavsiktligt upprepade anrop ofarliga och ger rensningen en tydlig ägare.

Åtgärd Beteende som krävs
Första mount() Anslut vyn och registrera en uppsättning lyssnare.
Ytterligare mount() när vyn är monterad Returnera utan att ändra något.
unmount() Ta bort monteringsperiodens lyssnare och koppla sedan bort vyn.
Ytterligare unmount() Returnera utan att ändra något.
mount() efter avmontering Registrera en ny uppsättning med en ny styrenhet; bevara räknarna.
destroy() Avmontera permanent; senare montering ger ett fel.

Det här är designval för applikationen. Webbläsaren tillhandahåller registrering och borttagning av händelser; vyn står för livscykelmetoderna och deras ordning. I en större applikation bör komponenten eller routern som anropar mount() också ordna motsvarande unmount().

En ny styrenhet är viktig eftersom en avbruten signal inte kan aktiveras igen. När addEventListener() får en redan avbruten signal returnerar DOM-registreringsalgoritmen utan att lägga till lyssnaren. Om du återanvänder den gamla styrenheten kan det därför hända att kontrollerna i en återöppnad vy inte fungerar. DOM-standarden: lägga till en händelselyssnare.

Bygg ett komplett exempel i Chrome på din Mac

Öppna en ny Chrome-flik, skriv about:blank i adressfältet och tryck på Retur. När sidan är aktiv trycker du på Kommando + Alternativ + J för att öppna Konsol. Google beskriver den här Mac-genvägen i sin tangentbordsreferens för Chrome DevTools.

Kör hela programmet nedan i Konsol. Det skapar gränssnittet med JavaScript, så det behövs ingen dokumentmarkup, paketinstallation eller byggprocess. Chrome-konsolen kan köra JavaScript och ändra den aktuella sidans DOM, vilket visas i Googles handledning om att köra JavaScript i konsolen.

(() => {
  // Clean up the previous demo if this entire program is run again.
  window.lifecycleDemo?.destroy();

  const host = document.body;
  const root = document.createElement("section");
  const button = document.createElement("button");
  const output = document.createElement("p");

  button.type = "button";
  button.textContent = "Add one";
  root.append(button, output);

  let controller = null;
  let destroyed = false;
  let clicks = 0;
  let resizes = 0;

  function render() {
    output.textContent =
      `Clicks: ${clicks}; resize events: ${resizes}`;
  }

  function mount() {
    if (destroyed) {
      throw new Error("This view has been destroyed.");
    }
    if (controller !== null) return;

    const next = new AbortController();
    const options = { signal: next.signal };

    try {
      button.addEventListener("click", () => {
        clicks += 1;
        render();
      }, options);

      window.addEventListener("resize", () => {
        resizes += 1;
        render();
      }, options);

      render();
      host.append(root);
      controller = next;
    } catch (error) {
      // Roll back any registrations made before setup failed.
      next.abort();
      root.remove();
      throw error;
    }
  }

  function unmount() {
    if (controller === null) return;
    controller.abort();
    controller = null;
    root.remove();
  }

  function destroy() {
    unmount();
    destroyed = true;
  }

  window.lifecycleDemo = {
    mount,
    unmount,
    destroy,
    button,
    get counts() {
      return { clicks, resizes };
    }
  };

  mount();
})();

Sidan innehåller nu en knapp med texten Lägg till ett och en räknarrad. Klicka på knappen: klickräknaren ska öka med ett. Ändra storleken på Chrome-fönstret: räknaren för storleksändringshändelser ska ändras. En fysisk storleksändring kan generera flera händelser, så räknaren visar antalet händelser, inte antalet musrörelser.

Knappens lyssnare och fönstrets lyssnare delar samma signal. Vid avmontering avbryts styrenheten innan rotobjektet kopplas bort. Nästa montering skapar en ny styrenhet och en ny uppsättning återanropsfunktioner. Räknarna finns kvar i vyns avslutning, så värdena bevaras när vyn öppnas igen. Om du förstör och kör om hela programmet skapas en ny instans.

Initieringsblocket hanterar också ett partiellt fel: om initieringen ger ett fel efter att en lyssnare har registrerats avbryter catch-blocket den nya styrenheten, kopplar bort rotobjektet och kastar felet igen. På så sätt kan en misslyckad montering inte lämna en del av initieringen aktiv utan att det märks.

Hur verifierar du att rensningen fungerar vid upprepad montering?

Börja med att prova livscykeln manuellt. Kör följande sekvens när du har installerat huvudprogrammet. Vyn försvinner, upprepad avmontering får ingen ytterligare effekt och när du öppnar vyn igen visas den med sina tidigare räknarvärden.

window.lifecycleDemo.unmount();
window.lifecycleDemo.unmount();
window.lifecycleDemo.mount();
window.lifecycleDemo.mount();

Kör sedan den här fullständiga kontrollen i samma konsol. Den förutsätter att huvudprogrammet ovan har körts. Varje cykel anropar medvetet initiering och rensning två gånger, skickar en händelse till varje mål medan vyn är monterad och skickar samma händelser medan den är avmonterad. Vid avvikelse kastas ett fel omedelbart.

(() => {
  const view = window.lifecycleDemo;
  if (!view) throw new Error("Run the main program first.");

  function check(actual, expected, label) {
    if (actual !== expected) {
      throw new Error(`${label}: expected ${expected}, got ${actual}`);
    }
  }

  for (let cycle = 0; cycle < 20; cycle += 1) {
    view.mount();
    view.mount();
    const before = view.counts;

    view.button.click();
    window.dispatchEvent(new Event("resize"));

    check(view.counts.clicks, before.clicks + 1, "Mounted click");
    check(view.counts.resizes, before.resizes + 1, "Mounted resize");

    view.unmount();
    view.unmount();
    const stopped = view.counts;

    // The button reference remains available while detached.
    view.button.click();
    window.dispatchEvent(new Event("resize"));

    check(view.counts.clicks, stopped.clicks, "Unmounted click");
    check(view.counts.resizes, stopped.resizes, "Unmounted resize");
  }

  view.mount();
  console.log("Passed 20 mount/unmount cycles.");
})();

Den syntetiska storleksändringshändelsen ger ett deterministiskt förväntat resultat utan att fönstret behöver flyttas. Kontrollen av den bortkopplade knappen är användbar eftersom exemplet medvetet behåller knappen för återmontering. Båda kontrollerna måste godkännas: om du bara testar den synliga knappen undersöker du inte fönstrets lyssnare. Testet lägger till 20 i varje räknare och avslutar med vyn monterad.

För direkt inspektion tillhandahåller Chrome getEventListeners(object) i DevTools-konsolen. Den returnerar de lyssnare som registrerats på det angivna objektet, grupperade efter händelsetyp. Det här är ett DevTools-verktyg, så använd det inte i applikationskod. Referens för Chrome Console Utilities API.

När du har kört huvudprogrammet jämför den här inspektionssekvensen registreringarna före och efter rensning. På den tomma demonstrationssidan kan du förvänta dig en klicklyssnare på knappen och en storleksändringslyssnare på fönstret när vyn är monterad, och sedan noll av vardera när den är avmonterad.

(() => {
  const view = window.lifecycleDemo;
  if (!view) throw new Error("Run the main program first.");

  view.mount();
  console.log("Mounted", {
    button: (getEventListeners(view.button).click ?? []).length,
    window: (getEventListeners(window).resize ?? []).length
  });

  view.unmount();
  console.log("Unmounted", {
    button: (getEventListeners(view.button).click ?? []).length,
    window: (getEventListeners(window).resize ?? []).length
  });

  view.mount();
})();

På en befintlig applikationssida kan annan kod äga fönsterlyssnare. Jämför skillnaden före och efter och inspektera återanropsfunktionerna i stället för att förutsätta att det totala antalet måste bli noll.

När bör du använda explicit borttagning i stället?

Använd removeEventListener() när du integrerar befintlig kod som redan lagrar återanropsfunktioner eller när miljön som krävs inte stöder lyssnaralternativet signal. Spara den ursprungliga målnoden, händelsetypen, återanropsreferensen och capture-värdet så att du kan rensa upp. MDN anger att capture-värdet måste stämma, medan alternativ som passive inte avgör matchningen vid borttagning. MDN:s referens för removeEventListener().

Spara till exempel en bunden återanropsfunktion en gång om initieringen använder bind(). Om du anropar bind() igen vid rensning får du en annan funktionsreferens. På samma sätt får du inte tillbaka den funktion som registrerades tidigare genom att skriva om samma arrow-funktion i anropet för borttagning. Håll den explicita rensningen intill initieringen så att de utvecklas tillsammans.

Styrenhetsmönstret omfattar de inbyggda lyssnare som registreras med dess signal. Om en vy även skapar timers, observatörer eller biblioteksprenumerationer ska du koppla deras dokumenterade rensningsåtgärder till samma avmonteringsgräns. Att skicka en signal till inbyggda lyssnare innebär inte att orelaterade resurser rensas.

Vad bör du kontrollera om återanropen fortfarande mångdubblas?

Utgå från symtomet för att avgränsa undersökningen i stället för att ändra flera livscykelmetoder på en gång.

Symtom Nästa sak att kontrollera
Ett klick ökar räknaren med två Leta efter en andra registreringsväg eller en andra aktiv vyinstans.
Kontrollerna slutar fungera när vyn öppnas igen Kontrollera om initieringen återanvände en avbruten signal.
Räknarna ändras efter avmontering Kontrollera alla händelsemål och om varje registrering fick den avsedda signalen.
Explicit borttagning ändrar ingenting Jämför målnoden, händelsetypen, återanropsfunktionens identitet och capture-värdet.
Den första åtgärden fungerar, men senare åtgärder gör ingenting Kontrollera om lyssnaren använder once: true.

Alternativet once tar bort en lyssnare efter dess första anrop. Det kan passa för en interaktion som verkligen bara ska ske en gång, men det beskriver inte en vy som är monterad under en viss livslängd: händelsen kanske inte inträffar före avmontering. MDN beskriver både once och signalbaserad borttagning i referensen för addEventListener()-alternativ.

När du är klar med demonstrationen kör du window.lifecycleDemo.destroy() för att koppla bort vyn och ta bort dess lyssnare. För din egen vy bör du följa samma praktiska kontrakt: en aktiv initiering, upprepningsbar rensning, nya registreringar när vyn öppnas igen och ett test som omfattar både synliga kontroller och händelsemål med längre livslängd.

Källor