Abschnitt 12 · Lektion 7
Listener wieder entfernen
Ein Listener ist nicht für die Ewigkeit. Manchmal soll etwas nur einmal reagieren, manchmal soll ein ganzer Bereich aufhören zuzuhören.
Der Haken an removeEventListener
<p><button id="benannt" type="button">Benannte Funktion</button> <span id="a">0</span></p>
<p><button id="direkt" type="button">Arrow direkt hingeschrieben</button> <span id="b">0</span></p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
} let a = 0;
let b = 0;
// Eine benannte Funktion: es gibt sie genau einmal.
function zaehleA() {
a = a + 1;
document.getElementById("a").textContent = String(a);
}
const benannt = document.getElementById("benannt");
benannt.addEventListener("click", zaehleA);
benannt.removeEventListener("click", zaehleA);
console.log("Der erste Knopf reagiert jetzt nicht mehr.");
// Und hier zweimal dieselbe Schreibweise, aber zwei Funktionen.
const direkt = document.getElementById("direkt");
direkt.addEventListener("click", () => {
b = b + 1;
document.getElementById("b").textContent = String(b);
});
direkt.removeEventListener("click", () => {
b = b + 1;
document.getElementById("b").textContent = String(b);
});
console.log("Der zweite Knopf zählt weiter. Probier beide aus.");
// Der Beweis in einer Zeile:
console.log("gleich?", (() => 1).toString() === (() => 1).toString());
console.log("und als Wert?", (() => 1) === (() => 1)); removeEventListener("click", zaehleA) löst den Listener wieder. Damit das klappt, müssen drei Dinge übereinstimmen: das Element, der Name des Ereignisses und die Funktion selbst.
Der dritte Punkt ist die Pointe dieser Lektion. Gemeint ist nicht „eine Funktion, die genauso aussieht”, sondern dieselbe. Zwei Arrow Functions mit identischem Inhalt sind zwei verschiedene Werte, genau wie zwei Objekte mit denselben Eigenschaften. Das ist derselbe Vergleich wie in Lektion 6.4.
Deshalb lässt sich eine direkt hingeschriebene Arrow Function nie entfernen. Beim addEventListener entsteht eine, beim removeEventListener entsteht eine zweite, und die kennt der Browser nicht. Einen Fehler gibt es dabei nicht: Er sucht, findet nichts, und tut nichts.
Die Eingabezeile der Console beantwortet das in einer Zeile: (() => 1) === (() => 1) antwortet false, zwei Funktionen, so gleich sie aussehen. zaehleA === zaehleA antwortet true, denn die benannte gibt es genau einmal, und genau deshalb findet removeEventListener sie wieder.
Die Lösung ist, die Funktion vorher zu benennen. Dann gibt es sie genau einmal, und beide Aufrufe meinen dieselbe.
once für den Einmal-Fall
<p><button id="einmal" type="button">Nur einmal</button> <span id="a">0</span></p>
<p><button id="immer" type="button">Immer</button> <span id="b">0</span></p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
} let a = 0;
let b = 0;
// once: true entfernt den Listener nach dem ersten Feuern
// von selbst. Man muss ihn nicht mehr aufheben.
document.getElementById("einmal").addEventListener(
"click",
() => {
a = a + 1;
document.getElementById("a").textContent = String(a);
console.log("einmal gelaufen, jetzt bin ich weg");
},
{ once: true }
);
document.getElementById("immer").addEventListener("click", () => {
b = b + 1;
document.getElementById("b").textContent = String(b);
});
console.log("Drück beide Knöpfe mehrmals."); Ein Listener, der nur beim ersten Mal reagieren soll, braucht kein removeEventListener und auch keinen eigenen Zähler. Es gibt eine Option dafür.
{ once: true } als drittes Argument sagt dem Browser: Ruf den Handler einmal auf und wirf ihn danach weg. Er räumt selbst auf.
Das ist kürzer als die Alternative und, wichtiger, es kann nicht auseinanderlaufen. Ein selbst geführter Zähler mit if (schonGelaufen) return; funktioniert auch, hält den Listener aber am Leben und verteilt die Bedingung im Code.
AbortController für mehrere auf einmal
<p><button id="eins" type="button">Eins</button> <button id="zwei" type="button">Zwei</button></p>
<p><button id="aus" type="button">Alles abschalten</button></p>
<p id="ausgabe">bereit</p> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
p {
margin: 0 0 0.75rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
} const ausgabe = document.getElementById("ausgabe");
// Ein Controller, ein Signal, beliebig viele Listener.
const steuerung = new AbortController();
const optionen = { signal: steuerung.signal };
document.getElementById("eins").addEventListener(
"click",
() => { ausgabe.textContent = "Eins geklickt"; },
optionen
);
document.getElementById("zwei").addEventListener(
"click",
() => { ausgabe.textContent = "Zwei geklickt"; },
optionen
);
// Ein Aufruf löst alle auf einmal.
document.getElementById("aus").addEventListener("click", () => {
steuerung.abort();
ausgabe.textContent = "abgeschaltet";
console.log("beide Listener sind weg");
});
console.log("Drück Eins und Zwei, dann Abschalten, dann wieder Eins."); Wenn ein ganzer Bereich aufhören soll zuzuhören, wird das einzelne Entfernen mühsam: Jede Funktion muss einen Namen haben, jeder Aufruf muss sich das Element merken.
Der moderne Weg geht über einen AbortController. Du legst einen an, gibst sein signal allen Listenern als Option mit, und ein einziges steuerung.abort() löst sie alle gleichzeitig.
Denselben Controller kennst du später aus Abschnitt 16, dort bricht er eine laufende Anfrage ab. Es ist genau derselbe Mechanismus für zwei ganz verschiedene Dinge.
Ein Controller ist verbraucht, sobald er ausgelöst hat. Für ein erneutes Zuhören brauchst du einen neuen, und die Listener müssen neu gesetzt werden.
Warum das überhaupt zählt
Ein Listener hält eine Verbindung zum Element und zu allem, was der Handler von außen benutzt. Solange er hängt, kann der Speicher davon nicht freigegeben werden.
Bei einer Seite, die nach einem Klick ohnehin neu lädt, spielt das keine Rolle. Bei einer Anwendung, die stundenlang offen bleibt und dabei laufend Bereiche auf- und abbaut, summiert es sich. Das nennt man ein Speicherleck, und die Ursache ist fast immer ein Listener, an den niemand mehr gedacht hat.
Die gute Nachricht: Wer delegiert wie in Lektion 12.6, hat das Problem gar nicht erst. Ein Listener am Container, der bleibt, und die Einträge darunter kommen und gehen ohne eigene Zuhörer.
Zum Mitnehmen
removeEventListener braucht genau dieselbe Funktion. Eine direkt hingeschriebene Arrow Function ist beim zweiten Mal eine andere, deshalb lässt sie sich nie entfernen.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe im Editor, direkt im Browser geprüft
Öffnet sich mit dem Basis Konto.