Ereignis-Bubbling
Wir haben gesehen, dass eine Webseite aus Elementen besteht – Überschriften, Textabsätzen, Bildern, Buttons und so weiter – und dass Sie Ereignisse überwachen können, die bei diesen Elementen auftreten. Zum Beispiel könnten Sie einen Listener zu einem Button hinzufügen, der ausgeführt wird, wenn der Benutzer auf den Button klickt.
Wir haben auch gesehen, dass diese Elemente verschachtelt sein können: Ein <button>
könnte zum Beispiel innerhalb eines <div>
-Elements platziert werden. In diesem Fall würden wir das <div>
-Element als Elternelement bezeichnen und das <button>
als Kindelement.
In diesem Kapitel betrachten wir Ereignis-Bubbling – das ist das, was passiert, wenn Sie einen Ereignislistener auf ein Elternelement setzen und der Benutzer auf das Kindelement klickt.
Voraussetzungen: | Ein Verständnis von HTML, den Grundlagen von CSS, und Vertrautheit mit den JavaScript-Grundlagen, die in den vorherigen Lektionen behandelt wurden. |
---|---|
Lernziele: |
|
Einführung in das Ereignis-Bubbling
Lassen Sie uns Ereignis-Bubbling anhand eines Beispiels einführen und definieren.
Setzen eines Listeners auf ein Elternelement
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. Wir sagen, dass hier das <div>
-Element der Elternteil des Elements ist, das es enthält. Was passiert, wenn wir einen Klick-Ereignishandler auf das Elternteil setzen und dann 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 werden sehen, dass das Elternteil ein Klick-Ereignis auslöst, wenn der Benutzer den Button klickt:
You clicked on a DIV element
Das macht Sinn: Der Button befindet sich innerhalb des <div>
, also klicken Sie implizit auch auf das Element, in dem er sich befindet.
Bubbling-Beispiel
Was passiert, wenn wir Ereignislistener auf den Button und das Elternelement hinzufügen?
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
Lassen Sie uns Klick-Ereignishandler zum Button, seinem Elternteil (dem <div>
) und dem <body>
-Element hinzufügen, das beide enthält:
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 werden sehen, dass alle drei Elemente ein Klick-Ereignis auslösen, wenn der Benutzer den Button klickt:
You clicked on a BUTTON element You clicked on a DIV element You clicked on a BODY element
In diesem Fall:
- wird zuerst der Klick auf den Button ausgelöst.
- gefolgt von einem Klick auf seinen Elternteil (das
<div>
-Element). - gefolgt von einem Klick auf den Elternteil des
<div>
-Elements (das<body>
-Element).
Wir beschreiben dies, indem wir sagen, dass das Ereignis von dem innersten Element, das geklickt wurde, aufsteigt.
Dieses Verhalten kann nützlich sein und auch unerwartete Probleme verursachen. In den nächsten Abschnitten werden wir ein Problem sehen, das es verursacht, und die Lösung finden.
Video-Player-Beispiel
In diesem Beispiel enthält unsere Seite ein Video, das zunächst verborgen ist, und einen Button mit der Beschriftung „Video anzeigen“. Wir wollen die folgende Interaktion:
- Wenn der Benutzer auf den Button „Video anzeigen“ klickt, wird das Kästchen, das das Video enthält, angezeigt, aber das Video wird noch nicht abgespielt.
- Wenn der Benutzer auf das Video klickt, beginnt das Video zu spielen.
- Wenn der Benutzer irgendwo im Kästchen außerhalb des Videos klickt, wird das Kästchen ausgeblendet.
Das HTML sieht so aus:
<button>Display video</button>
<div class="hidden">
<video>
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-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 beinhaltet:
- ein
<button>
-Element. - ein
<div>
-Element, das zunächst einclass="hidden"
-Attribut besitzt. - 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"));
Dies fügt drei 'click'
-Ereignislistener hinzu:
- einen auf dem
<button>
, der das<div>
zeigt, das das<video>
enthält. - einen auf dem
<video>
, der das Video startet. - einen auf dem
<div>
, der das Video ausblendet.
Lassen Sie uns sehen, wie das funktioniert:
Sie sollten sehen, dass wenn Sie den Button klicken, das Kästchen und das darin enthaltene Video angezeigt werden. Wenn Sie dann auf das Video klicken, beginnt das Video zu spielen, aber das Kästchen wird wieder ausgeblendet!
Das Video befindet sich innerhalb des <div>
– es ist ein Teil davon – also führt das Klicken auf das Video beide Ereignishandler aus, was dieses Verhalten verursacht.
Das Problem mit stopPropagation()
beheben
Wie wir im letzten Abschnitt gesehen haben, kann das Ereignis-Bubbling manchmal Probleme verursachen, aber es gibt eine Möglichkeit, es zu verhindern.
Das Event
-Objekt hat eine Funktion namens stopPropagation()
, die innen eines Ereignishandlers aufgerufen wird und das Aufsteigen des Ereignisses zu anderen Elementen verhindert.
Wir können unser derzeitiges Problem lösen, 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"));
Alles, was wir hier tun, ist stopPropagation()
auf dem Ereignisobjekt im Handler für das 'click'
-Ereignis des <video>
-Elements aufzurufen. Dies verhindert, dass das Ereignis zum Kästchen aufsteigt. Versuchen Sie nun, auf den Button und dann auf das Video zu klicken:
Ereignis-Capturing
Eine alternative Form der Ereignisausbreitung ist das Ereignis-Capturing. Dies ist wie das Ereignis-Bubbling, aber die Reihenfolge ist umgekehrt: Statt dass das Ereignis zuerst auf dem innersten Element ausgelöst wird, wird es auf dem äußersten Element ausgelöst und dann auf den jeweils tiefer verschachtelten Elementen, bis das Ziel erreicht ist.
Das Ereignis-Capturing ist standardmäßig deaktiviert. Um es zu aktivieren, müssen Sie die capture
-Option in addEventListener()
angeben.
Dieses Beispiel entspricht genau dem Bubbling-Beispiel, das wir zuvor gesehen haben, außer dass wir die capture
-Option verwendet haben:
<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 Nachrichten umgekehrt: Der <body>
-Ereignishandler wird zuerst ausgelöst, gefolgt vom <div>
-Ereignishandler, gefolgt vom <button>
-Ereignishandler:
You clicked on a BODY element You clicked on a DIV element You clicked on a BUTTON element
Warum sich mit Capturing und Bubbling beschäftigen? In den schlechten alten Zeiten, als Browser viel weniger miteinander kompatibel waren als heute, benutzte Netscape nur Ereignis-Capturing und Internet Explorer nur Ereignis-Bubbling. Als das W3C beschloss, das Verhalten zu standardisieren und einen Konsens zu erreichen, führten sie dieses System ein, das beide Mechanismen enthalten sollte, was moderne Browser implementieren.
Standardmäßig sind fast alle Ereignishandler in der Bubbling-Phase registriert, und das ergibt die meiste Zeit mehr Sinn.
Ereignis-Delegierung
Im letzten Abschnitt haben wir ein Problem durch das Ereignis-Bubbling und wie man es beheben kann betrachtet. Ereignis-Bubbling ist jedoch nicht nur lästig: es kann sehr nützlich sein. Insbesondere ermöglicht es die Ereignis-Delegierung. Bei dieser Praxis setzen wir, wenn wir möchten, dass ein Code ausgeführt wird, wenn der Benutzer mit einem Element einer großen Menge von Kindelementen interagiert, den Ereignis-Listener auf ihrem Elternteil und lassen Ereignisse, die auf ihnen passieren, zu ihrem Elternteil aufsteigen, anstatt den Ereignis-Listener auf jedem einzelnen Kind zu setzen.
Gehen wir zurück zu unserem ersten Beispiel, bei dem wir die Hintergrundfarbe der gesamten Seite verändern, wenn der Benutzer auf einen Button klickt. Angenommen, die Seite ist stattdessen in 16 Kacheln unterteilt, und wir möchten jede Kachel in einer zufälligen Farbe setzen, wenn der Benutzer auf diese Kachel klickt.
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>
Wir haben ein bisschen CSS, um die Größe und Position der Kacheln festzulegen:
#container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 100px;
}
Jetzt im JavaScript könnten wir einen Klick-Ereignishandler für jede Kachel hinzufügen. Aber eine viel einfachere und effizientere Option ist es, den Klick-Ereignishandler auf das Elternelement zu setzen und sich auf Ereignis-Bubbling zu verlassen, um sicherzustellen, dass der Handler ausgeführt wird, wenn der Benutzer auf eine Kachel klickt:
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 ist wie folgt (versuchen Sie hier und da zu klicken):
Hinweis:
In diesem Beispiel verwenden wir event.target
, um das Element zu erhalten, das das Ziel des Ereignisses war (das heißt, das innerste Element). Wenn wir auf das Element zugreifen wollten, das dieses Ereignis behandelt hat (in diesem Fall der Container), könnten wir event.currentTarget
verwenden.
Hinweis: Sehen Sie sich useful-eventtarget.html für den vollständigen Quellcode an; sehen Sie es auch hier live laufen.
target
und currentTarget
Wenn Sie die in dieser Seite eingeführten Beispiele genau betrachten, werden Sie sehen, dass wir zwei verschiedene Eigenschaften des Ereignisobjekts verwenden, um auf das Element zuzugreifen, das geklickt wurde. Im Abschnitt Setzen eines Listeners auf ein Elternelement verwenden wir event.currentTarget
. Im Abschnitt Ereignis-Delegierung dagegen verwenden wir event.target
.
Der Unterschied ist, dass target
sich auf das Element bezieht, auf dem das Ereignis ursprünglich ausgelöst wurde, während currentTarget
sich auf das Element bezieht, an das dieser Ereignis-Handler angehängt wurde.
Während target
gleich bleibt, während ein Ereignis aufsteigt, wird currentTarget
für Ereignis-Handler unterschiedlich sein, die an verschiedene Elemente im Hierarchiebaum angehängt sind.
Wir können das sehen, wenn wir das Bubbling-Beispiel leicht anpassen. Wir verwenden das gleiche HTML wie zuvor:
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
Das JavaScript ist fast gleich, außer dass wir sowohl target
als auch currentTarget
protokollieren:
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, dass wenn wir den Button klicken, target
jedes Mal das Button-Element ist, egal ob der Ereignis-Handler an den Button selbst, an das <div>
oder an das <body>
angehängt ist. currentTarget
jedoch identifiziert das Element, dessen Ereignis-Handler wir gerade ausführen:
Die target
-Eigenschaft wird häufig in der Ereignis-Delegierung verwendet, wie in unserem Ereignis-Delegierungs-Beispiel oben.
Testen Sie Ihre Fähigkeiten!
Sie haben das Ende dieses Artikels erreicht, aber können Sie sich die wichtigsten Informationen merken? Um zu überprüfen, ob Sie diese Informationen behalten haben, bevor Sie fortfahren — sehen Sie sich Testen Sie Ihre Fähigkeiten: Ereignisse an.
Zusammenfassung
Sie sollten jetzt alles wissen, was Sie über Webereignisse zu diesem frühen Zeitpunkt wissen müssen. Wie erwähnt, gehören Ereignisse nicht wirklich zum Kern von JavaScript — sie sind in Browser-Web-APIs definiert.
Es ist auch wichtig zu verstehen, dass die verschiedenen Kontexte, in denen JavaScript verwendet wird, unterschiedliche Ereignismodelle haben — von Web-APIs zu anderen Bereichen wie Browser-WebExtensions und Node.js (serverseitiges JavaScript). Wir erwarten nicht, dass Sie jetzt alle diese Bereiche verstehen, aber es hilft sicherlich, die Grundlagen von Ereignissen zu verstehen, während Sie mit dem Erlernen der Webentwicklung voranschreiten.
Als Nächstes finden Sie eine Herausforderung, die Ihr Verständnis der letzten Themen testen wird.
Siehe auch
- domevents.dev
-
Eine nützliche interaktive Playground-Anwendung, die das Lernen über das Verhalten des DOM-Ereignissystems durch Erkundung ermöglicht.
- Ereignisreferenz
-
Die Haupt-MDN-Ereignisreferenz.
- Ereignisreihenfolge
-
Eine ausgezeichnet und detailliert ausgearbeitete Diskussion über Capturing und Bubbling von Peter-Paul Koch.