preventDefault
und stopPropagation
: Wann welche Methode verwendet werden sollte und was genau sie bewirkt.
Event.stopPropagation() und Event.preventDefault()
Die JavaScript-Ereignisbehandlung ist oft unkompliziert. Das gilt insbesondere bei einer einfachen (relativ flachen) HTML-Struktur. Etwas komplizierter wird es, wenn Ereignisse durch eine Elementhierarchie wandern (oder sich fortpflanzen). In diesem Fall wenden sich Entwickler in der Regel an stopPropagation()
und/oder preventDefault()
, um die auftretenden Probleme zu beheben. Wenn Sie schon einmal gedacht haben: „Ich probiere einfach preventDefault()
aus und wenn das nicht funktioniert, versuche ich es mit stopPropagation()
. Wenn das auch nicht funktioniert, probiere ich beides aus“, dann ist dieser Artikel genau richtig für Sie. Ich erkläre Ihnen genau, was die einzelnen Methoden bewirken, wann Sie welche verwenden sollten, und stelle Ihnen eine Vielzahl von Beispielen zur Verfügung, die Sie ausprobieren können. Ich möchte Ihre Verwirrung ein für alle Mal beseitigen.
Bevor wir uns jedoch näher damit befassen, sollten wir kurz die beiden Arten der Ereignisbehandlung in JavaScript erwähnen (in allen modernen Browsern – Internet Explorer vor Version 9 unterstützte die Ereigniserfassung überhaupt nicht).
Ereignisstile (Aufzeichnen und Weitergeben)
Alle modernen Browser unterstützen die Ereigniserfassung, sie wird aber von Entwicklern nur selten verwendet.
Interessanterweise war es die einzige Form von Ereignissen, die Netscape ursprünglich unterstützte. Der größte Rivale von Netscape, Microsoft Internet Explorer, unterstützte die Ereigniserfassung überhaupt nicht, sondern nur eine andere Art von Ereignis, das sogenannte Ereignis-Bubbling. Als das W3C gegründet wurde, erkannte es die Vorteile beider Ereignisstile und erklärte, dass Browser beide über einen dritten Parameter für die addEventListener
-Methode unterstützen sollten. Ursprünglich war dieser Parameter nur ein boolescher Wert. Alle modernen Browser unterstützen jedoch ein options
-Objekt als dritten Parameter, mit dem Sie unter anderem angeben können, ob die Ereigniserfassung verwendet werden soll:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Das options
-Objekt und das zugehörige capture
-Attribut sind optional. Wenn beides weggelassen wird, ist der Standardwert für capture
false
. Das bedeutet, dass Ereignis-Bubbling verwendet wird.
Ereigniserfassung
Was bedeutet es, wenn der Ereignis-Handler „in der Capturing-Phase überwacht“? Um das zu verstehen, müssen wir wissen, wie Ereignisse entstehen und wie sie sich ausbreiten. Das gilt für alle Ereignisse, auch wenn Sie als Entwickler sie nicht nutzen, nicht für wichtig halten oder nicht darüber nachdenken.
Alle Ereignisse beginnen im Fenster und durchlaufen zuerst die Erfassungsphase. Das bedeutet, dass ein Ereignis, wenn es ausgelöst wird, zuerst das Fenster startet und dann „nach unten“ zum Zielelement wechselt. Das passiert auch, wenn Sie nur in der Phase der Ideenentwicklung zuhören. Sehen wir uns das folgende Beispiel für Markup und JavaScript an:
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('#C was clicked');
},
true,
);
Wenn ein Nutzer auf Element #C
klickt, wird ein Ereignis gesendet, das von window
ausgeht. Dieses Ereignis wird dann wie unten dargestellt an seine Nachkommen weitergegeben:
window
=> document
=> <html>
=> <body>
=> usw., bis das Ziel erreicht ist.
Es spielt keine Rolle, ob beim Element window
, document
, <html>
oder <body>
(oder einem anderen Element auf dem Weg zum Ziel) kein Klickereignis überwacht wird. Ein Ereignis stammt weiterhin vom window
und durchläuft den beschriebenen Pfad.
In unserem Beispiel wird das Klickereignis dann übertragen (dies ist ein wichtiges Wort, da es sich direkt auf die Funktionsweise der stopPropagation()
-Methode bezieht und später in diesem Dokument erläutert wird) vom window
über jedes Element zwischen window
und #C
zum Zielelement (in diesem Fall #C
).
Das bedeutet, dass das Klickereignis bei window
beginnt und der Browser die folgenden Fragen stellt:
„Werden in der Capturing-Phase Listener für ein Klickereignis auf die window
eingerichtet?“ In diesem Fall werden die entsprechenden Ereignis-Handler ausgelöst. In unserem Beispiel ist das nicht der Fall, sodass keine Handler ausgelöst werden.
Als Nächstes wird das Ereignis an die document
weitergegeben und der Browser fragt: „Lauscht irgendetwas in der Erfassungsphase auf ein Klickereignis auf der document
?“ In diesem Fall werden die entsprechenden Ereignis-Handler ausgelöst.
Als Nächstes wird das Ereignis an das <html>
-Element weitergeleitet und der Browser fragt: „Werden in der Erfassungsphase Klicks auf das <html>
-Element überwacht?“ In diesem Fall werden die entsprechenden Ereignishandler ausgelöst.
Als Nächstes wird das Ereignis an das <body>
-Element weitergegeben und der Browser fragt: „Werden in der Erfassungsphase Klickereignisse für das <body>
-Element überwacht?“ In diesem Fall werden die entsprechenden Ereignis-Handler ausgelöst.
Als Nächstes wird das Ereignis an das Element #A
weitergegeben. Der Browser fragt noch einmal: „Wartet in der Capturing-Phase irgendetwas auf ein Klickereignis auf #A
?“ Wenn ja, werden die entsprechenden Ereignishandler ausgelöst.
Als Nächstes wird das Ereignis an das #B
-Element weitergegeben und dieselbe Frage wird gestellt.
Schließlich erreicht das Ereignis sein Ziel und der Browser fragt: „Wartet in der Capturing-Phase irgendetwas auf ein Klickereignis auf dem Element #C
?“ Die Antwort lautet diesmal „Ja“. Diese kurze Zeitspanne, in der sich das Ereignis am Ziel befindet, wird als „Zielphase“ bezeichnet. An diesem Punkt wird der Ereignis-Handler ausgelöst, der Browser sendet „#C wurde angeklickt“ an console.log und dann sind wir fertig, richtig?
Falsch! Wir sind noch lange nicht fertig. Der Prozess wird fortgesetzt, aber jetzt geht es in die Phase der Ideenentwicklung.
Ereigniskaskaden
Der Browser fragt:
„Warte ich in der Bubble-Phase auf ein Klickereignis für #C
?“ Achten Sie hier genau darauf.
Es ist möglich, sowohl in der Capturing-Phase als auch in der Bubbling-Phase auf Klicks (oder einen beliebigen Ereignistyp) zu warten. Wenn Sie Ereignis-Handler in beiden Phasen verbunden haben (z.B. durch zweimaliges Aufrufen von .addEventListener()
, einmal mit capture = true
und einmal mit capture = false
), werden beide Ereignis-Handler für dasselbe Element ausgelöst. Es ist jedoch auch wichtig zu beachten, dass sie in verschiedenen Phasen ausgelöst werden (eine in der Erfassungsphase und eine in der Bubble-Phase).
Als Nächstes wird das Ereignis weitergegeben (häufiger als „Bubbling“ bezeichnet, weil es so aussieht, als würde das Ereignis „den DOM-Baum hinauf“ wandern) an sein übergeordnetes Element #B
. Der Browser fragt dann: „Lauscht irgendetwas in der Bubbling-Phase auf Klicke bei #B
?“ In unserem Beispiel ist das nicht der Fall, sodass keine Handler ausgelöst werden.
Als Nächstes wird das Ereignis an #A
weitergeleitet und der Browser fragt: „Werden in der Bubble-Phase Klickereignisse auf #A
überwacht?“
Als Nächstes wird das Ereignis an <body>
gesendet: „Werden in der Bubble-Phase Click-Events auf das <body>
-Element überwacht?“
Als Nächstes das <html>
-Element: „Werden in der Bubble-Phase Click-Events für das <html>
-Element überwacht?
Als Nächstes die document
: „Werden in der Phase der Ideenentwicklung Click-Events auf der document
erfasst?“
Schließlich die window
: „Werden in der Bubble-Phase Click-Events für das Fenster überwacht?“
Geschafft! Das war ein langer Weg und unser Ereignis ist wahrscheinlich schon sehr müde. Aber glauben Sie mir: So sieht der Prozess aus, den jedes Ereignis durchläuft. In den meisten Fällen wird das nie bemerkt, da sich Entwickler in der Regel nur für eine der Ereignisphasen interessieren (und das ist in der Regel die Phase der Blasenbildung).
Es lohnt sich, etwas Zeit mit der Ereigniserfassung und dem Ereignis-Bubbling zu verbringen und sich Notizen in der Konsole zu machen, wenn Handler ausgelöst werden. Es ist sehr aufschlussreich, den Pfad eines Ereignisses zu sehen. Hier ist ein Beispiel, bei dem in beiden Phasen auf jedes Element geachtet wird.
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.addEventListener(
'click',
function (e) {
console.log('click on document in capturing phase');
},
true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in capturing phase');
},
true,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in capturing phase');
},
true,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in capturing phase');
},
true,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in capturing phase');
},
true,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in capturing phase');
},
true,
);
document.addEventListener(
'click',
function (e) {
console.log('click on document in bubbling phase');
},
false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in bubbling phase');
},
false,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in bubbling phase');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in bubbling phase');
},
false,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in bubbling phase');
},
false,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in bubbling phase');
},
false,
);
Die Konsolenausgabe hängt davon ab, auf welches Element Sie klicken. Wenn Sie auf das „tiefste“ Element im DOM-Baum (das #C
-Element) klicken, wird jeder einzelne dieser Ereignishandler ausgelöst. Hier ist das Konsolenausgabeelement #C
mit einem kleinen CSS-Styling, damit es leichter zu erkennen ist, welches Element welches ist. Auch ein Screenshot ist zu sehen:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"
In der Live-Demo unten können Sie damit interaktiv spielen. Klicken Sie auf das Element #C
und beobachten Sie die Konsolenausgabe.
event.stopPropagation()
Nachdem wir nun wissen, woher Ereignisse stammen und wie sie sich sowohl in der Capturing-Phase als auch in der Bubbling-Phase durch das DOM bewegen (d.h. weitergeben), können wir uns event.stopPropagation()
widmen.
Die stopPropagation()
-Methode kann bei (den meisten) nativen DOM-Ereignissen aufgerufen werden. Ich sage „die meisten“, weil es einige gibt, bei denen der Aufruf dieser Methode nichts bewirkt, da das Ereignis nicht weitergeleitet wird. Ereignisse wie focus
, blur
, load
, scroll
und einige andere fallen in diese Kategorie. Sie können stopPropagation()
aufrufen, aber es passiert nichts Interessantes, da diese Ereignisse nicht weitergegeben werden.
Aber was macht stopPropagation
?
Es macht genau das, was der Name sagt. Wenn Sie es aufrufen, wird das Ereignis ab diesem Zeitpunkt nicht mehr an Elemente weitergegeben, die es sonst erreichen würde. Das gilt in beide Richtungen (Aufzeichnung und Weitergabe). Wenn Sie stopPropagation()
also irgendwo in der Erfassungsphase aufrufen, gelangt das Ereignis nie in die Zielphase oder die Phase des Aufsteigens. Wenn Sie es in der Phase der Ideenentwicklung aufrufen, hat es bereits die Phase der Ideenfindung durchlaufen, aber es wird ab dem Zeitpunkt, an dem Sie es aufrufen, nicht mehr „aufsteigen“.
Denken wir noch einmal an unser Beispiel-Markup. Was würde Ihrer Meinung nach passieren, wenn wir stopPropagation()
in der Erfassungsphase beim Element #B
aufrufen?
Das würde zu folgender Ausgabe führen:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
In der Live-Demo unten können Sie damit interaktiv spielen. Klicken Sie in der Live-Demo auf das Element #C
und beobachten Sie die Konsolenausgabe.
Wie wäre es damit, die Ausbreitung in der Phase der Blasenbildung bei #A
zu stoppen? Das würde zu folgender Ausgabe führen:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
In der Live-Demo unten können Sie damit interaktiv spielen. Klicken Sie in der Live-Demo auf das Element #C
und beobachten Sie die Konsolenausgabe.
Nur noch eine Frage, aus Spaß. Was passiert, wenn wir stopPropagation()
in der Zielphase für #C
aufrufen?
Die „Zielphase“ ist der Zeitraum, in dem sich das Ereignis am Ziel befindet. Das würde zu folgender Ausgabe führen:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
Der Ereignis-Handler für #C
, in dem wir „Klick auf #C in der Erfassungsphase“ erfassen, wird weiter ausgeführt, derjenige, in dem wir „Klick auf #C in der Bubble-Phase“ erfassen, jedoch nicht. Das sollte absolut logisch sein. Wir haben stopPropagation()
von der ersten aufgerufen. An diesem Punkt wird die Weiterleitung des Ereignisses beendet.
In der Live-Demo unten können Sie damit interaktiv spielen. Klicken Sie in der Live-Demo auf das Element #C
und beobachten Sie die Konsolenausgabe.
Ich empfehle Ihnen, mit diesen Livedemos zu experimentieren. Klicken Sie nur auf das #A
- oder das body
-Element. Versuchen Sie, vorherzusagen, was passieren wird, und beobachten Sie dann, ob Sie richtig liegen. An diesem Punkt sollten Sie ziemlich genau vorhersagen können.
event.stopImmediatePropagation()
Um welche Methode handelt es sich? Sie ähnelt stopPropagation
, verhindert aber nicht, dass ein Ereignis an Nachkommen (Aufzeichnung) oder Vorfahren (Aufsteigen) weitergegeben wird. Diese Methode gilt nur, wenn an ein einzelnes Element mehrere Ereignishandler angeschlossen sind. Da addEventListener()
einen Multicast-Ereignisstil unterstützt, ist es durchaus möglich, einen Ereignishandler mehrmals mit einem einzelnen Element zu verknüpfen. In den meisten Browsern werden die Ereignishandler in diesem Fall in der Reihenfolge ausgeführt, in der sie verbunden wurden. Wenn Sie stopImmediatePropagation()
aufrufen, werden alle nachfolgenden Handler nicht ausgelöst. Dazu ein Beispiel:
<html>
<body>
<div id="A">I am the #A element</div>
</body>
</html>
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run first!');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run second!');
e.stopImmediatePropagation();
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
},
false,
);
Das obige Beispiel führt zur folgenden Console-Ausgabe:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
Der dritte Ereignis-Handler wird nie ausgeführt, da der zweite Ereignis-Handler e.stopImmediatePropagation()
aufruft. Wenn wir stattdessen e.stopPropagation()
aufgerufen hätten, würde der dritte Handler trotzdem ausgeführt.
event.preventDefault()
Was macht stopPropagation()
, wenn ein Ereignis nicht „nach unten“ (Aufzeichnung) oder „nach oben“ (Aufsteigen) weitergeleitet wird?preventDefault()
Anscheinend funktioniert es ähnlich. Ist das der Fall?
Nicht wirklich. Die beiden Begriffe werden oft verwechselt, haben aber eigentlich nicht viel miteinander zu tun.
Wenn Sie preventDefault()
sehen, denken Sie im Kopf an das Wort „Aktion“. Denken Sie daran, dass Sie die Standardaktion verhindern möchten.
Und was ist die Standardaktion? Leider ist die Antwort darauf nicht ganz so eindeutig, da sie stark von der Kombination aus Element und Ereignis abhängt. Und um das Ganze noch verwirrender zu machen: Manchmal gibt es gar keine Standardaktion.
Beginnen wir mit einem sehr einfachen Beispiel. Was passiert, wenn Sie auf einen Link auf einer Webseite klicken? Natürlich erwarten Sie, dass der Browser zur mit diesem Link angegebenen URL weitergeleitet wird.
In diesem Fall ist das Element ein Anker-Tag und das Ereignis ein Klickereignis. Diese Kombination (<a>
+ click
) hat die Standardaktion „Zum href des Links wechseln“. Was, wenn Sie verhindern möchten, dass der Browser diese Standardaktion ausführt? Angenommen, Sie möchten verhindern, dass der Browser zur URL wechselt, die durch das href
-Attribut des <a>
-Elements angegeben ist. Das ist das, was preventDefault()
für Sie tun wird. Betrachten Sie dieses Beispiel:
<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
'click',
function (e) {
e.preventDefault();
console.log('Maybe we should just play some of their music right here instead?');
},
false,
);
In der Live-Demo unten können Sie damit interaktiv spielen. Klicken Sie auf den Link The Avett Brothers und achten Sie auf die Konsolenausgabe. Sie werden nicht zur Website der Avett Brothers weitergeleitet.
Normalerweise würde ein Klick auf den Link „The Avett Brothers“ zu www.theavettbrothers.com
führen. In diesem Fall haben wir jedoch einen Klick-Ereignis-Handler mit dem <a>
-Element verbunden und angegeben, dass die Standardaktion verhindert werden soll. Wenn ein Nutzer also auf diesen Link klickt, wird er nicht weitergeleitet. Stattdessen wird in der Konsole einfach protokolliert: „Vielleicht sollten wir stattdessen einfach etwas von seiner Musik abspielen?“
Mit welchen anderen Element-/Ereigniskombinationen können Sie die Standardaktion verhindern? Ich kann unmöglich alle aufzählen und manchmal muss man einfach ausprobieren. Hier sind einige Beispiele:
<form>
-Element + Ereignis „submit“:preventDefault()
Mit dieser Kombination wird verhindert, dass ein Formular gesendet wird. Dies ist nützlich, wenn Sie eine Validierung durchführen möchten und bei einem Fehler preventDefault bedingt aufrufen können, um das Senden des Formulars zu verhindern.<a>
-Element + „click“-Ereignis:preventDefault()
verhindert bei dieser Kombination, dass der Browser zur im href-Attribut des<a>
-Elements angegebenen URL wechselt.document
+ Ereignis „mousewheel“:preventDefault()
für diese Kombination verhindert das Scrollen der Seite mit dem Mausrad. Das Scrollen mit der Tastatur funktioniert jedoch weiterhin.
↜ Dazu mussaddEventListener()
mit{ passive: false }
aufgerufen werden.document
+ „keydown“-Ereignis:preventDefault()
für diese Kombination ist tödlich. Dadurch wird die Seite weitgehend unbrauchbar, da das Scrollen, das Tabulatoren und das Hervorheben von Tastaturelementen nicht mehr möglich ist.document
+ Ereignis „mousedown“:preventDefault()
für diese Kombination verhindert das Hervorheben von Text mit der Maus und jede andere „Standardaktion“, die mit gedrückter Maustaste ausgeführt wird.<input>
-Element + „keypress“-Ereignis:preventDefault()
für diese Kombination verhindert, dass vom Nutzer eingegebene Zeichen das Eingabeelement erreichen. Dies ist jedoch nicht empfehlenswert, da es dafür nur selten einen triftigen Grund gibt.document
+ Ereignis „contextmenu“:preventDefault()
Mit dieser Kombination wird verhindert, dass das native Kontextmenü des Browsers angezeigt wird, wenn ein Nutzer mit der rechten Maustaste klickt oder lange drückt (oder auf eine andere Weise, auf die ein Kontextmenü angezeigt werden könnte).
Diese Liste ist keineswegs vollständig, gibt Ihnen aber hoffentlich eine gute Vorstellung davon, wie preventDefault()
verwendet werden kann.
Einen lustigen Streich?
Was passiert, wenn Sie in der Aufnahmephase stopPropagation()
und preventDefault()
drücken, beginnend beim Dokument? Das kann zu lustigen Situationen führen. Das folgende Code-Snippet macht jede Webseite nahezu unbrauchbar:
function preventEverything(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });
Ich weiß nicht wirklich, warum Sie das tun möchten (außer vielleicht, um jemandem einen Streich zu spielen), aber es ist nützlich, darüber nachzudenken, was hier passiert, und zu erkennen, warum diese Situation entsteht.
Alle Ereignisse stammen von window
. In diesem Snippet verhindern wir, dass click
-, keydown
-, mousedown
-, contextmenu
- und mousewheel
-Ereignisse an Elemente gelangen, die möglicherweise darauf warten. Außerdem wird stopImmediatePropagation
aufgerufen, damit alle Handler, die nach diesem mit dem Dokument verbunden sind, ebenfalls verhindert werden.
stopPropagation()
und stopImmediatePropagation()
sind nicht (zumindest nicht hauptsächlich) dafür verantwortlich, dass die Seite unbrauchbar ist. Sie verhindern einfach, dass Ereignisse dorthin gelangen, wo sie sonst hingehören würden.
Wir rufen aber auch preventDefault()
auf, was wie Sie sich erinnern, die Standard-Aktion verhindert. So werden alle Standardaktionen (z. B. Scrollen mit dem Mausrad, Scrollen oder Hervorheben mit der Tastatur, Tabulatortaste, Klicken auf Links, Anzeige des Kontextmenüs usw.) verhindert, sodass die Seite ziemlich nutzlos ist.
Livedemos
In der eingebetteten Demo unten können Sie sich alle Beispiele aus diesem Artikel noch einmal ansehen.
Danksagungen
Hero-Image von Tom Wilson auf Unsplash.