Event-Bubbling
Wir haben gesehen, dass eine Webseite aus Elementen besteht – Überschriften, Textabsätze, Bilder, Schaltflächen und so weiter – und dass Sie Ereignisse, die auf diesen Elementen passieren, abhören können. Zum Beispiel könnten Sie einen Listener zu einer Schaltfläche hinzufügen, und dieser wird ausgeführt, wenn der Benutzer die Schaltfläche anklickt.
Wir haben auch gesehen, dass diese Elemente ineinander verschachtelt sein können: zum Beispiel könnte ein <button>
in einem <div>
-Element platziert werden. In diesem Fall würden wir das <div>
-Element ein Elternelement nennen und das <button>
ein Kindelement.
In diesem Kapitel werden wir uns mit Event-Bubbling befassen – das ist das, was passiert, wenn Sie einen Event-Listener zu einem Elternelement hinzufügen und der Benutzer das Kindelement anklickt.
Voraussetzungen: | Ein Verständnis von HTML und den Grundlagen von CSS, sowie Vertrautheit mit den JavaScript-Grundlagen, wie sie in früheren Lektionen behandelt wurden. |
---|---|
Lernergebnisse: |
|
Einführung in Event-Bubbling
Lassen Sie uns Event-Bubbling anhand eines Beispiels einführen und definieren.
Einen Listener auf ein Elternelement setzen
Betrachten Sie eine Webseite wie diese:
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
Hier befindet sich die Schaltfläche innerhalb eines anderen Elements, einem <div>
-Element. Wir sagen, dass das <div>
-Element hier das Elternelement des enthaltenen Elements ist. Was passiert, wenn wir einen Klick-Ereignishandler zum Eltern hinzufügen und dann die Schaltfläche anklicken?
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 Elternelement ein Klick-Ereignis auslöst, wenn der Benutzer auf die Schaltfläche klickt:
You clicked on a DIV element
Das ergibt Sinn: Die Schaltfläche befindet sich im <div>
, sodass Sie beim Anklicken der Schaltfläche auch implizit das Element anklicken, in dem sie sich befindet.
Bubbling-Beispiel
Was passiert, wenn wir sowohl der Schaltfläche als auch dem Elternteil Ereignis-Listener hinzufügen?
<body>
<div id="container">
<button>Click me!</button>
</div>
<pre id="output"></pre>
</body>
Versuchen wir, Klick-Ereignishandler zur Schaltfläche, ihrem Elternteil (dem <div>
) und dem <body>
-Element hinzuzufü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 die Schaltfläche anklickt:
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 die Schaltfläche ausgelöst.
- gefolgt vom Klick auf ihren Elternteil (das
<div>
-Element). - gefolgt vom Klick auf den Elternteil des
<div>
-Elements (das<body>
-Element).
Wir beschreiben dies, indem wir sagen, dass das Ereignis vom innersten angeklickten Element nach oben aufsteigt.
Dieses Verhalten kann nützlich sein und auch unerwartete Probleme verursachen. In den nächsten Abschnitten werden wir ein Problem davon sehen und die Lösung finden.
Video-Player-Beispiel
In diesem Beispiel enthält unsere Seite ein Video, das anfangs versteckt ist, und eine Schaltfläche mit der Beschriftung "Video anzeigen". Wir möchten die folgende Interaktion:
- Wenn der Benutzer auf die Schaltfläche "Video anzeigen" klickt, soll die Box mit dem Video angezeigt werden, das Video soll jedoch noch nicht abgespielt werden.
- Wenn der Benutzer auf das Video klickt, soll das Video abgespielt werden.
- Wenn der Benutzer irgendwo in der Box außerhalb des Videos klickt, soll die Box ausgeblendet werden.
Der HTML-Code 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>
Darin enthalten sind:
- ein
<button>
-Element. - ein
<div>
-Element, das anfangs einclass="hidden"
-Attribut hat. - ein
<video>
-Element, das im<div>
-Element verschachtelt ist.
Wir verwenden CSS, um Elemente mit gesetzter "hidden"
-Klasse zu verstecken.
Der JavaScript-Code 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'
Event-Listener hinzu:
- einen zum
<button>
, der das<div>
, das das<video>
-Element enthält, anzeigt. - einen zum
<video>
, der das Video startet. - einen zum
<div>
, der das Video ausblendet.
Sehen wir uns an, wie das funktioniert:
Sie sollten sehen, dass wenn Sie die Schaltfläche anklicken, die Box und das darin enthaltene Video angezeigt werden. Aber dann, wenn Sie das Video anklicken, beginnt das Video zu spielen, aber die Box wird wieder versteckt!
Das Video ist innerhalb des <div>
— es ist Teil davon — also führt das Anklicken des Videos beide Event-Handler aus, was zu diesem Verhalten führt.
Das Problem mit stopPropagation()
beheben
Wie wir im letzten Abschnitt gesehen haben, kann Event-Bubbling manchmal Probleme verursachen, aber es gibt eine Möglichkeit, dies zu verhindern.
Das Event
-Objekt hat eine darauf verfügbare Funktion namens stopPropagation()
, die, wenn sie innerhalb eines Ereignishandlers aufgerufen wird, das Aufsteigen des Ereignisses zu anderen Elementen verhindert.
Wir können unser aktuelles Problem beheben, indem wir das JavaScript so ä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 das Ereignis-Objekt im Handler für das 'click'
-Ereignis des <video>
-Elements aufzurufen. Dies wird das Ereignis daran hindern, zur Box umzusteigen. Versuchen Sie es nun, indem Sie auf die Schaltfläche und dann auf das Video klicken:
Event-Capture
Eine alternative Form der Ereignisweitergabe ist das Event-Capture. Dies ähnelt dem Event-Bubbling, jedoch ist die Reihenfolge umgekehrt: Anstatt dass das Ereignis zuerst auf dem innersten anvisierten Element ausgelöst wird und dann auf jeweils weniger verschachtelten Elementen, wird das Ereignis zunächst auf dem am wenigsten verschachtelten Element ausgelöst und dann auf jeweils mehr verschachtelten Elementen, bis das Ziel erreicht ist.
Event-Capture ist standardmäßig deaktiviert. Um es zu aktivieren, müssen Sie die capture
-Option in addEventListener()
übergeben.
Dieses Beispiel ähnelt dem Bubbling-Beispiel, das wir früher 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>
-Eventhandler wird zuerst ausgelöst, gefolgt vom <div>
-Eventhandler, gefolgt vom <button>
-Eventhandler:
You clicked on a BODY element You clicked on a DIV element You clicked on a BUTTON element
Warum sich mit både capture und bubbling beschäftigen? In den schlechten alten Zeiten, als Browser viel weniger kompatibel zueinander waren als heute, verwendete Netscape nur Event-Capturing, und der Internet Explorer verwendete nur Event-Bubbling. Als das W3C beschloss, das Verhalten zu standardisieren und einen Konsens zu erreichen, endeten sie mit diesem System, das beide beinhaltete, was moderne Browser implementieren.
Standardmäßig werden fast alle Event-Handler in der Bubbling-Phase registriert, und dies macht die meiste Zeit mehr Sinn.
Event-Delegation
Im letzten Abschnitt haben wir ein durch Event-Bubbling verursachtes Problem betrachtet und wie man es behebt. Event-Bubbling ist jedoch nicht nur störend: Es kann sehr nützlich sein. Insbesondere ermöglicht es Event-Delegation. In dieser Praxis, wenn wir möchten, dass Code ausgeführt wird, wenn der Benutzer mit einem von vielen Kindelementen interagiert, setzen wir den Event-Listener auf ihr Elternelement und lassen das Ereignis, das auf ihnen passiert, zu ihrem Elternelement aufsteigen, anstatt den Event-Listener auf jedem Kind einzeln zu setzen.
Lassen Sie uns zu unserem ersten Beispiel zurückkehren, in dem wir die Hintergrundfarbe der gesamten Seite festlegen, wenn der Benutzer eine Schaltfläche anklickt. Angenommen, die Seite ist stattdessen in 16 Kacheln unterteilt, und wir möchten jede Kachel auf eine zufällige Farbe setzen, wenn der Benutzer diese Kachel anklickt.
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 wenig CSS, um die Größe und Position der Kacheln festzulegen:
#container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 100px;
}
Jetzt könnten wir in JavaScript für jede Kachel einen Klick-Eventhandler hinzufügen. Eine viel einfachere und effizientere Option ist jedoch, den Klick-Eventhandler auf das Elternelement zu setzen und sich auf das Event-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, darauf zu klicken):
Hinweis:
In diesem Beispiel verwenden wir event.target
, um das Element zu erhalten, das das Ziel des Ereignisses war (also 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: Siehe useful-eventtarget.html für den vollständigen Quellcode; sehen Sie ihn auch hier live laufen.
target
und currentTarget
Wenn Sie genau hinsehen bei den Beispielen, die wir in dieser Seite vorgestellt haben, werden Sie sehen, dass wir zwei verschiedene Eigenschaften des Ereignisobjekts verwenden, um auf das angeklickte Element zuzugreifen. In Einen Listener auf ein Elternelement setzen verwenden wir event.currentTarget
. Jedoch verwenden wir in Event-Delegation event.target
.
Der Unterschied ist, dass sich target
auf das Element bezieht, auf dem das Ereignis ursprünglich ausgelöst wurde, während sich currentTarget
auf das Element bezieht, an das dieser Ereignis-Handler angehängt ist.
Während target
während eines Event-Bubbling-Prozesses gleich bleibt, wird currentTarget
für Event-Handler, die an verschiedene Elemente in der Hierarchie angehängt sind, unterschiedlich sein.
Wir können das sehen, wenn wir das Bubbling-Beispiel oben leicht anpassen. Wir verwenden das gleiche HTML wie vorher:
<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 die Schaltfläche anklicken, target
jedes Mal das Schaltflächen-Element ist, egal ob der Event-Handler an der Schaltfläche selbst, am <div>
oder am <body>
angehängt ist. currentTarget
hingegen identifiziert das Element, dessen Event-Handler wir aktuell ausführen:
Die target
-Eigenschaft wird häufig in der Event-Delegation verwendet, wie in unserem Event-Delegation Beispiel oben zu sehen ist.
Zusammenfassung
Sie sollten nun alles wissen, was Sie über Webereignisse in diesem frühen Stadium wissen müssen. Wie erwähnt, sind Ereignisse nicht wirklich Teil der Kern-JavaScript-Sprache – sie sind in den Web-APIs des Browsers definiert.
Im nächsten Artikel geben wir Ihnen einige Tests, mit denen Sie überprüfen können, wie gut Sie all die Informationen, die wir Ihnen über Ereignisse gegeben haben, verstanden und behalten haben.
Siehe auch
- domevents.dev
-
Eine nützliche interaktive Playground-App, die es ermöglicht, das Verhalten des DOM-Ereignissystems durch Erkundung zu erlernen.
- DOM-Ereignisse
-
Ein umfassender Leitfaden zum Verständnis und Umgang mit Ereignissen.
- Ereignisreihenfolge
-
Eine hervorragend detaillierte Diskussion von Capturing und Bubbling von Peter-Paul Koch.