Få klickhanterare att fortsätta fungera när en vy ersätter sitt DOM-innehåll genom att koppla en enda lyssnare till ett överordnat element som finns kvar efter uppdateringen. Den här guiden visar hur du implementerar och kontrollerar mönstret i Safari på en Mac med en liten, syntetisk panel för val av brickor.
Uppgiften är specifik: bygg om innehållet i en JavaScript-vy och behåll samtidigt knapparnas respons och markeringen. Du behöver grundläggande kunskaper i JavaScript och en aktuell Safari-webbläsare. Exemplet använder vanliga webbläsar-API:er och kräver varken ramverk eller paketinstallation.
Varför slutar klick fungera efter att DOM ersatts?
En lyssnare som registrerats med addEventListener() hör till händelsemålet där den registrerades. Om du tilldelar en containers innerHTML tolkas den angivna uppmärkningen och dess underordnade noder ersätts. Därför ärver en ny knapp inte en lyssnare som kopplats till den tidigare knappen, även om dess ID och etikett är desamma. Se MDN:s referens för innerHTML och DOM-standardens modell för händelselyssnare.
Det är användbart att skilja mellan att ersätta en containers innehåll och att ersätta själva containern. Om koden behåller containern och bara bygger om dess barn kan en lyssnare på containern sitta kvar. Om en yttre rendering även ersätter containern måste du välja en förfader som finns kvar eller koppla en lyssnare när den nya containern initieras.
För vanliga klickhändelser som bubblar uppåt kan en förälder hantera interaktioner som börjar i dess underordnade noder genom händelsedelegering. I stället för att koppla varje ersättningsknapp kopplar du den stabila föräldern och identifierar den relevanta knappen när händelsen inträffar. MDN:s guide till händelsebubbling beskriver mönstret.
Var bör lyssnaren placeras?
Börja med att hitta den exakta DOM-gräns som renderingen ersätter. Välj sedan den mest begränsade lämpliga föräldern utanför den gränsen. Följande vägledning tillämpar det dokumenterade ersättnings- och lyssnarbeteendet på vanliga uppdateringsmönster. MDN: replaceChildren(), DOM-standard: EventTarget.
| Din uppdatering | Placering av lyssnaren | Krav på livscykel |
|---|---|---|
| Bygg om barnen till en befintlig vyrot | Delegera klick från den roten | Registrera en gång när roten skapas |
| Ersätt även vyroten | Delegera från en yttre container som finns kvar, eller koppla lyssnaren till den nya roten | Rensa bort den föregående vyns registrering |
| Skapa knappar som behöver egna lyssnare | Koppla en lyssnare till varje ny knapp under konstruktionen | Slutför kopplingen innan de nya kontrollerna blir tillgängliga |
| Läs in hela dokumentet igen | Initiera lyssnare i det nya dokumentet | Kör den vanliga sidinitieringen igen |
I exemplet nedan består strukturen av en vyrot med en rubrik, status, en ordnad uppsättning brickval och en uppdateringskontroll. Varje rendering bygger om dessa barn i samma ordning. Endast vyroten, lyssnaren och JavaScript-tillståndet finns kvar.
Bygg en komplett vy som tål ersättningar i Safari
Förbered först en tillfällig webbläsarflik. Öppna Settings… i Safari på din Mac, välj Advanced och aktivera Show features for web developers. Öppna Web Inspector via Develop → Show Web Inspector eller tryck på Option-Command-I. Stegen finns dokumenterade i WebKits guide till att aktivera Web Inspector.
Gå till about:blank, öppna Web Inspector och välj Console. Klistra in hela programmet nedan i prompten och kör det. WebKit dokumenterar att prompten i Console kör JavaScript i den valda exekveringskontexten för den inspekterade sidan. Använd den tomma fliken så att demonstrationen hålls åtskild från din app.
Det här är ett komplett webbläsarprogram: det skapar sin egen DOM, definierar exempeldata, monterar vyn och tillhandahåller en rensningsmetod. Det ersätter vyns barn med replaceChildren(), som tar emot nodobjekt och ersätter den befintliga uppsättningen barn. MDN:s metodreferens beskriver funktionen.
(() => {
window.domClickDemo?.destroy();
const root = document.createElement("section");
root.id = "dom-click-demo";
const controller = new AbortController();
const items = [
{ id: "a", label: "Tile A" },
{ id: "b", label: "Tile B" },
{ id: "c", label: "Tile C" }
];
const state = { selected: "None", refreshes: 0 };
function makeButton(action, label, id) {
const button = document.createElement("button");
button.type = "button";
button.dataset.action = action;
if (id !== undefined) button.dataset.id = id;
const caption = document.createElement("span");
caption.textContent = label;
button.append(caption);
return button;
}
function render() {
const heading = document.createElement("h2");
heading.textContent = "Tile picker";
const status = document.createElement("p");
status.textContent =
`Selected: ${state.selected}; refreshes: ${state.refreshes}`;
const list = document.createElement("ul");
for (const item of items) {
const row = document.createElement("li");
row.append(makeButton("select", item.label, item.id));
list.append(row);
}
const refresh = makeButton("refresh", "Replace view content");
root.replaceChildren(heading, status, list, refresh);
}
function handleClick(event) {
if (!(event.target instanceof Element)) return;
const button = event.target.closest("button[data-action]");
if (!button || !root.contains(button) || button.disabled) return;
if (button.dataset.action === "select") {
const item = items.find(item => item.id === button.dataset.id);
if (!item) return;
state.selected = item.label;
} else if (button.dataset.action === "refresh") {
state.refreshes += 1;
} else {
return;
}
render();
}
root.addEventListener("click", handleClick, {
signal: controller.signal
});
render();
document.body.append(root);
window.domClickDemo = {
root,
destroy() {
controller.abort();
root.remove();
}
};
})();
Lyssnaren registreras utanför render(). Varje val och uppdatering skapar nya knappar, men alla knappar ligger fortfarande under den ursprungliga roten. Tillståndsobjektet finns också utanför det renderade delträdet, så att statusstycket ersätts återställer varken den valda brickan eller antalet uppdateringar.
Den nästlade bildtexten är avsiktlig. Ett klick på knappens text kan träffa dess underordnade element. closest("button[data-action]") söker igenom målet och dess förfäder efter den matchande kontrollen, så att hanteraren inte bara fungerar när själva knappen är händelsemålet. MDN:s referens för closest() beskriver sökningen och resultatet null när ingen matchning hittas.
Inneslutningskontrollen säkerställer att den matchande kontrollen finns inom den här vyns gräns. DOM-standarden definierar contains() utifrån om den angivna noden är noden själv eller en av dess underordnade noder. DOM-standarden: Node.contains().
Håll initiering, rendering och borttagning åtskilda
Exemplet har tre tydliga ansvarsområden. Initieringen skapar roten och registrerar lyssnaren. Renderingen uppdaterar rotens barn utifrån tillståndet. Borttagningen tar bort lyssnaren och roten. Den uppdelningen utgör implementationens livscykelkontrakt; behåll motsvarande ansvar när du anpassar den till din app.
Lyssnaren får en AbortSignal, och destroy() avbryter dess styrenhet. Avbrytandet tar bort den kopplade registreringen, vilket dokumenteras i MDN:s referens för addEventListener(). Om du kör hela programmet igen tas den föregående demonstrationen först bort, så att flera demovyer inte samlas på hög.
Lägg inte en ny anonym lyssnare inuti varje rendering. Upprepade anonyma funktionsuttryck skapar olika funktionsobjekt, så deras registreringar kan samlas på en rot som finns kvar. Om du återanvänder samma återanrop, händelsetyp och inställning för fångst undviker du dubbla registreringar, men det blir tydligare vem som ansvarar om kopplingen sker under initieringen. MDN: addEventListener(), DOM-standarden: lägga till lyssnare.
Kontrollera upprepade ersättningar, markering och rensning
Använd följande sekvens för att testa exemplet. De förväntade resultaten följer av programmets uttryckliga tillståndsuppdateringar; de är inte påståenden om att ett Safari-test har genomförts.
- Klicka på
Tile B. Statusen bör visaSelected: Tile B; refreshes: 0. - Klicka på
Replace view contenttre gånger. Statusen bör visaSelected: Tile B; refreshes: 3. - Klicka på texten
Tile C. Statusen bör ändras tillSelected: Tile C; refreshes: 3. - Klicka på rubriken eller statusstycket. Inget av värdena bör ändras.
- Kör
window.domClickDemo.destroy()i Console. Demonstrationen bör försvinna. - Kör hela programmet igen. En enda ny vy bör visas med
Selected: None; refreshes: 0.
De tre första kontrollerna täcker ersättning, bevarat tillstånd och nästlade klickmål. Den fjärde kontrollerar filtrering av åtgärder. De två sista kontrollerar borttagnings- och initieringsflödet. Tillsammans testar de den faktiska livscykeln i stället för att bara bekräfta att de ursprungliga knapparna fungerar.
Programmets JavaScript-syntax kontrollerades med Node, och en separat deterministisk EventTarget-kontroll bekräftade att återanrop dedupliceras och tas bort via en avbrytningssignal. Webbläsarens DOM-rendering och interaktion i Safari kördes inte i den kontrollen; använd sekvensen ovan för att verifiera dem på din Mac.
Vad gör jag om hanteraren fortfarande inte fungerar i min app?
Undersök den första punkt där beteendet avviker. Behåll exemplet avgränsat medan du kontrollerar appens ersättningsgräns, händelseväg och tillståndsuppdateringar.
| Symptom | Kontrollera först | Lämplig korrigering |
|---|---|---|
| Klick fungerar tills en yttre vy uppdateras | Ersattes även lyssnarens rot? | Koppla lyssnaren till den nya roten vid initiering eller välj en förälder som finns kvar |
| Ett klick registreras, men ingen åtgärd körs | Matchar den nya kontrollen fortfarande väljaren och åtgärdsvärdet? | Bevara kontrollens väljare och åtgärdskontrakt |
| Klick på knapptexten beter sig annorlunda | Kontrollerar hanteraren endast det exakta målet? | Hitta kontrollen med closest() |
| Ett klick uppdaterar tillståndet flera gånger | Finns flera registreringar eller både direkta och delegerade hanterare aktiva? | Låt en initieringsväg ansvara för åtgärden |
| Hanteraren körs, men visningen återställs | Skapas tillståndet på nytt inuti renderingen? | Behåll nödvändigt tillstånd utanför det delträd som ersätts |
Granska också händelsespridningen. En hanterare på en underordnad nod som anropar stopPropagation() kan hindra en händelse från att bubbla upp till din delegerade lyssnare. MDN förklarar både hur spridning stoppas och händelsedelegering. Ta reda på varför spridningen stoppas innan du ändrar lyssnarfas, eftersom en ändring av fas även ändrar körordningen.
Om du måste använda direkta knapplyssnare ska du koppla dem när du skapar ersättningsknapparna. En engångssökning och kopplingsrunda kan inte initiera noder som skapas vid senare renderingar: registreringar hör till sina händelsemål. På samma sätt kan delegering inte bevara en lyssnare på en rot som renderaren kastar bort. Dessa gränser följer av DOM-standardens modell för lyssnare.
För den här uppgiften är den hållbara lösningen en stabil vyrot, en delegerad lyssnare, tillstånd som lagras utanför de renderade barnen och en uttrycklig rensningsväg. Ta reda på vilka noder som finns kvar efter varje uppdatering, placera lyssnaren därefter och verifiera flera efterföljande ersättningar innan du integrerar ändringen i den större vyn.
Källor
- Aktivera Web Inspector — WebKit.
- Web Inspectors gränssnitt — WebKit.
- Element: innerHTML-egenskapen — MDN Web Docs.
- Händelsebubbling — MDN Web Docs.
- Element: metoden replaceChildren() — MDN Web Docs.
- Element: metoden closest() — MDN Web Docs.
- EventTarget: metoden addEventListener() — MDN Web Docs.
- DOM-standarden — WHATWG.