Event Bubbling
Wir haben gesehen, dass eine Webseite aus Elementen besteht – Überschriften, Textabsätzen, Bildern, Buttons und so weiter – und dass Sie auf Ereignisse reagieren können, die bei diesen Elementen auftreten. Sie könnten beispielsweise einem Button einen Event Listener hinzufügen, der ausgeführt wird, wenn jemand auf den Button klickt.
Wir haben auch gesehen, dass diese Elemente ineinander verschachtelt sein können: Beispielsweise könnte ein <button> innerhalb eines <div>-Elements stehen. In diesem Fall bezeichnen wir das <div>-Element als Elternelement und das <button>-Element als Kindelement.
In diesem Kapitel sehen wir uns Event Bubbling an – also das, was passiert, wenn Sie einem Elternelement einen Event Listener hinzufügen und auf das Kindelement geklickt wird.
| Voraussetzungen: | Verständnis von HTML und den Grundlagen von CSS sowie Vertrautheit mit den JavaScript-Grundlagen aus den vorherigen Lektionen. |
|---|---|
| Lernziele: |
|
Einführung in Event Bubbling
Sehen wir uns Event Bubbling anhand eines Beispiels an.
Einen Event Listener auf einem Elternelement registrieren
Betrachten Sie eine Webseite wie diese:
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
Hier befindet sich der Button innerhalb eines anderen Elements, eines <div>-Elements. Das <div>-Element ist das Elternelement des Elements, das es enthält. Was passiert, wenn wir dem Elternelement einen Click-Event-Handler hinzufügen und dann auf den Button klicken?
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
container.addEventListener("click", handleClick);
Sie sehen, dass beim Klick auf den Button ein Click-Event für das Elternelement ausgelöst wird:
You clicked on a DIV element
Das ist nachvollziehbar: Der Button befindet sich innerhalb des <div>-Elements. Wenn Sie auf den Button klicken, klicken Sie damit indirekt auch auf das Element, in dem er sich befindet.
Beispiel für Bubbling
Was passiert, wenn wir sowohl dem Button als auch dem Elternelement Event Listener hinzufügen?
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
Fügen wir dem Button, seinem Elternelement (dem <div>) und dem <body>-Element, das beide enthält, Click-Event-Handler hinzu:
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
Sie sehen, dass beim Klick auf den Button für alle drei Elemente ein Click-Event ausgelöst wird:
You clicked on a BUTTON element You clicked on a DIV element You clicked on a BODY element
In diesem Fall geschieht Folgendes:
- Zuerst wird das Click-Event auf dem Button ausgelöst.
- Danach folgt das Click-Event auf seinem Elternelement (dem
<div>-Element). - Anschließend folgt das Click-Event auf dem Elternelement des
<div>-Elements (dem<body>-Element).
Wir beschreiben das so: Das Ereignis steigt vom innersten Element, auf das geklickt wurde, nach oben.
Dieses Verhalten kann nützlich sein, aber auch unerwartete Probleme verursachen. In den nächsten Abschnitten sehen wir uns ein solches Problem und seine Lösung an.
Beispiel mit einem Videoplayer
In diesem Beispiel enthält unsere Seite ein Video, das zunächst verborgen ist, und einen Button mit der Beschriftung „Video anzeigen“. Wir möchten folgendes Verhalten erreichen:
- Wenn auf den Button „Video anzeigen“ geklickt wird, soll die Box mit dem Video angezeigt werden, ohne dass das Video bereits abgespielt wird.
- Wenn auf das Video geklickt wird, soll die Wiedergabe beginnen.
- Wenn innerhalb der Box außerhalb des Videos auf eine Stelle geklickt wird, soll die Box ausgeblendet werden.
Das HTML sieht so aus:
<button>Display video</button>
<div class="hidden">
<video>
<source src="/shared-assets/videos/flower.webm" type="video/webm" />
<p>
Your browser doesn't support HTML video. Here is a
<a href="rabbit320.mp4">link to the video</a> instead.
</p>
</video>
</div>
Es enthält:
- ein
<button>-Element. - ein
<div>-Element, das anfangs einclass="hidden"-Attribut hat. - ein
<video>-Element, das innerhalb des<div>-Elements verschachtelt ist.
Wir verwenden CSS, um Elemente mit der Klasse "hidden" auszublenden.
Das JavaScript sieht so aus:
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");
btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", () => video.play());
box.addEventListener("click", () => box.classList.add("hidden"));
Damit werden drei 'click'-Event-Listener hinzugefügt:
- einer auf dem
<button>, der das<div>mit dem<video>anzeigt. - einer auf dem
<video>, der die Wiedergabe des Videos startet. - einer auf dem
<div>, der das Video ausblendet.
Sehen wir uns an, wie das funktioniert:
Wenn Sie auf den Button klicken, sollten die Box und das darin enthaltene Video angezeigt werden. Wenn Sie dann aber auf das Video klicken, beginnt zwar die Wiedergabe, doch die Box wird wieder ausgeblendet!
Das Video befindet sich innerhalb des <div>-Elements – es ist Teil davon. Deshalb werden beim Klick auf das Video beide Event-Handler ausgeführt, was zu diesem Verhalten führt.
Das Problem mit stopPropagation() beheben
Wie wir im letzten Abschnitt gesehen haben, kann Event Bubbling manchmal Probleme verursachen. Es gibt jedoch eine Möglichkeit, es zu verhindern.
Das Event-Objekt stellt eine Funktion namens stopPropagation() bereit. Wird sie innerhalb eines Event-Handlers aufgerufen, verhindert sie, dass das Ereignis zu anderen Elementen aufsteigt.
Wir können unser Problem beheben, indem wir das JavaScript wie folgt ändern:
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");
btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", (event) => {
event.stopPropagation();
video.play();
});
box.addEventListener("click", () => box.classList.add("hidden"));
Hier rufen wir lediglich stopPropagation() auf dem Event-Objekt im Handler für das 'click'-Event des <video>-Elements auf. Dadurch steigt dieses Ereignis nicht mehr zur Box auf. Klicken Sie jetzt auf den Button und anschließend auf das Video:
Event Capturing
Eine andere Form der Ereignisweitergabe ist Event Capturing. Es ähnelt Event Bubbling, aber die Reihenfolge ist umgekehrt: Statt zuerst auf dem innersten Zielelement und anschließend auf immer weniger tief verschachtelten Elementen wird das Ereignis zuerst auf dem am wenigsten tief verschachtelten Element ausgelöst und dann auf immer tiefer verschachtelten Elementen, bis es das Zielelement erreicht.
Event Capturing ist standardmäßig deaktiviert. Um es zu aktivieren, müssen Sie die Option capture an addEventListener() übergeben.
Dieses Beispiel entspricht dem zuvor gezeigten Bubbling-Beispiel, verwendet jedoch die Option capture:
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
const output = document.querySelector("#output");
function handleClick(e) {
output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick, { capture: true });
container.addEventListener("click", handleClick, { capture: true });
button.addEventListener("click", handleClick);
In diesem Fall ist die Reihenfolge der Meldungen umgekehrt: Zuerst wird der Event-Handler des <body>-Elements ausgeführt, dann der des <div>-Elements und schließlich der des <button>-Elements:
You clicked on a BODY element You clicked on a DIV element You clicked on a BUTTON element
Warum gibt es sowohl Capturing als auch Bubbling? Früher, als Browser noch deutlich weniger miteinander kompatibel waren, verwendete Netscape nur Event Capturing und Internet Explorer nur Event Bubbling. Als das W3C versuchte, das Verhalten zu standardisieren und einen Konsens zu finden, entstand schließlich dieses System, das beides umfasst und von modernen Browsern implementiert wird.
Standardmäßig werden fast alle Event-Handler für die Bubbling-Phase registriert. Das ist in den meisten Fällen sinnvoller.
Event Delegation
Im letzten Abschnitt haben wir ein durch Event Bubbling verursachtes Problem und seine Lösung betrachtet. Event Bubbling ist aber nicht nur lästig: Es kann auch sehr nützlich sein. Insbesondere ermöglicht es Event Delegation. Wenn Code ausgeführt werden soll, sobald mit einem beliebigen von vielen Kindelementen interagiert wird, registrieren wir den Event Listener auf deren Elternelement. Die Ereignisse auf den Kindelementen steigen dann zum Elternelement auf, sodass wir nicht für jedes Kindelement einzeln einen Event Listener registrieren müssen.
Kehren wir zu unserem ersten Beispiel zurück, in dem wir die Hintergrundfarbe der gesamten Seite geändert haben, wenn auf einen Button geklickt wurde. Nehmen wir stattdessen an, die Seite sei in 16 Kacheln unterteilt, und wir möchten jeder Kachel eine zufällige Farbe geben, wenn auf sie geklickt wird.
Hier ist das HTML:
<div id="container">
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
</div>
Mit etwas CSS legen wir die Größe und Position der Kacheln fest:
#container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 100px;
}
Nun könnten wir in JavaScript für jede Kachel einen Click-Event-Handler hinzufügen. Viel einfacher und effizienter ist es jedoch, den Click-Event-Handler auf dem Elternelement zu registrieren und Event Bubbling dafür zu nutzen, dass der Handler beim Klick auf eine Kachel ausgeführt wird:
function random(number) {
return Math.floor(Math.random() * number);
}
function bgChange() {
const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
return rndCol;
}
const container = document.querySelector("#container");
container.addEventListener("click", (event) => {
event.target.style.backgroundColor = bgChange();
});
Das Ergebnis sieht so aus (probieren Sie aus, auf verschiedene Stellen zu klicken):
Hinweis:
In diesem Beispiel verwenden wir event.target, um das Element zu ermitteln, das Ziel des Ereignisses war (also das innerste Element). Wenn wir auf das Element zugreifen möchten, das dieses Ereignis verarbeitet hat (in diesem Fall den Container), können wir event.currentTarget verwenden.
target und currentTarget
Wenn Sie sich die Beispiele auf dieser Seite genauer ansehen, werden Sie feststellen, dass wir zwei verschiedene Eigenschaften des Event-Objekts verwenden, um auf das angeklickte Element zuzugreifen. In Einen Event Listener auf einem Elternelement registrieren verwenden wir event.currentTarget. Bei der Event Delegation verwenden wir dagegen event.target.
Der Unterschied besteht darin, dass target auf das Element verweist, auf dem das Ereignis ursprünglich ausgelöst wurde, während currentTarget auf das Element verweist, an dem dieser Event-Handler registriert ist.
Während target beim Aufsteigen eines Ereignisses gleich bleibt, unterscheidet sich currentTarget bei Event-Handlern, die an verschiedenen Elementen der Hierarchie registriert sind.
Das können wir sehen, wenn wir das obige Bubbling-Beispiel leicht anpassen. Wir verwenden dasselbe HTML wie zuvor:
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
Das JavaScript ist fast identisch, allerdings protokollieren wir sowohl target als auch currentTarget:
const output = document.querySelector("#output");
function handleClick(e) {
const logTarget = `Target: ${e.target.tagName}`;
const logCurrentTarget = `Current target: ${e.currentTarget.tagName}`;
output.textContent += `${logTarget}, ${logCurrentTarget}\n`;
}
const container = document.querySelector("#container");
const button = document.querySelector("button");
document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);
Beachten Sie: Wenn wir auf den Button klicken, ist target jedes Mal das Button-Element – unabhängig davon, ob der Event-Handler am Button selbst, am <div> oder am <body> registriert ist. currentTarget bezeichnet dagegen das Element, dessen Event-Handler gerade ausgeführt wird:
Die Eigenschaft target wird häufig bei Event Delegation verwendet, wie in unserem obigen Beispiel für Event Delegation.
Zusammenfassung
Sie sollten nun alles wissen, was Sie in dieser frühen Phase über Web-Events wissen müssen. Wie erwähnt, sind Events nicht Teil der JavaScript-Kernsprache – sie werden durch Web-APIs des Browsers definiert.
Im nächsten Artikel finden Sie einige Tests, mit denen Sie überprüfen können, wie gut Sie die Informationen über Events verstanden und behalten haben.
Siehe auch
- domevents.dev
-
Eine nützliche interaktive Anwendung, mit der Sie das Verhalten des DOM-Event-Systems erkunden und verstehen können.
- DOM-Events
-
Ein umfassender Leitfaden zum Verständnis und zur Verarbeitung von Events.
- Reihenfolge von Events
-
Eine ausführliche Erläuterung von Capturing und Bubbling von Peter-Paul Koch.