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. Wenn Ereignisse durch eine Hierarchie von Elementen bewegt werden (oder sich über sie ausbreiten), ist die Sache etwas komplizierter. In diesem Fall wenden sich Entwickler in der Regel an stopPropagation()
und/oder preventDefault()
, um die auftretenden Probleme zu lösen. 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. Das gilt für alle modernen Browser – der Internet Explorer vor Version 9 unterstützte die Ereigniserfassung überhaupt nicht.
Ereignisstile (Aufzeichnen und Bubblen)
Alle modernen Browser unterstützen die Ereigniserfassung, sie wird jedoch nur sehr selten von Entwicklern verwendet.
Interessanterweise war dies die einzige Ereignisform, die Netscape ursprünglich unterstützt hat. 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 Objekt options
und sein Attribut capture
sind optional. Wenn einer der Parameter weggelassen wird, ist der Standardwert für capture
false
. Das bedeutet, dass Event-Bubbling verwendet wird.
Ereigniserfassung
Was bedeutet es, wenn der Event-Handler in der Erfassungsphase zuhört? Um das zu verstehen, müssen wir wissen, wie Ereignisse entstehen und wie sie sich ausbreiten. Folgendes gilt für alle Ereignisse, auch wenn du als Entwickler es nicht nutzt, nicht darum bittest oder darüber nachdenkst.
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. Dies geschieht auch dann, wenn Sie nur in der Bubbling-Phase 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 die 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 Click-Event 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 Event-Handler 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.
Bubble von Terminen
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 „aufwärts“ durch den DOM-Baum wandern) an sein übergeordnetes Element #B
. Der Browser fragt dann: „Lauscht irgendetwas in der Bubbling-Phase auf Klickereignisse bei #B
?“ In unserem Beispiel ist nichts so,
also werden auch keine Handler ausgelöst.
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 in die Bubble mit <body>
aufgenommen: „Wird in der Bubbling-Phase irgendetwas auf 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
überwacht?“
Schließlich die window
: „Werden in der Bubble-Phase Click-Events für das Fenster überwacht?“
Geschafft! Das war ein langer Weg und unsere Veranstaltung ist inzwischen wahrscheinlich sehr müde. Aber ob du es glaubst oder nicht, das ist der Weg, den jedes Event durchläuft! Meistens wird dies nie bemerkt, da Entwickler in der Regel nur an der einen oder der anderen Ereignisphase interessiert sind (und normalerweise in der Bubbling-Phase).
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 Element #C
) klicken, sehen Sie jeden einzelnen dieser Event-Handler, die ausgelöst werden. Durch einige CSS-Stile soll deutlicher gemacht werden, welches Element in welchem ist. Hier sehen Sie das #C
-Element der Konsolenausgabe (mit einem Screenshot):
"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()
Da wir jetzt wissen, woher Ereignisse stammen und wie sie sich in der Erfassungsphase und der Bubbling-Phase durch das DOM bewegen (d.h. übertragen werden), können wir uns jetzt auf event.stopPropagation()
konzentrieren.
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 tut stopPropagation
?
Es macht genau das, was der Name sagt. Wenn Sie es aufrufen, wird das Ereignis ab diesem Zeitpunkt nicht mehr an Elemente weitergegeben, an die es andernfalls übertragen 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“.
Was würde Ihrer Meinung nach passieren, wenn wir noch einmal zu unserem Beispiel-Markup zurückkehren, wenn wir in der Erfassungsphase beim #B
-Element stopPropagation()
aufrufen würden?
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 das interaktiv ausprobieren. 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 das interaktiv ausprobieren. Klicken Sie in der Live-Demo auf das Element #C
und beobachten Sie die Konsolenausgabe.
Nur noch eine Frage, aus Spaß. Was passiert, wenn stopPropagation()
in der Zielphase für #C
aufgerufen wird?
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 Sinn ergeben. 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.
Bei diesen Livedemos können Sie herumspielen. 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()
Was ist diese seltsame und nicht häufig verwendete Methode? 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. Durch das Aufrufen von stopImmediatePropagation()
wird verhindert, dass nachfolgende Handler ausgelöst werden. 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 Event-Handler wird nie ausgeführt, da der zweite Event-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? 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 auf diese Frage nicht ganz so eindeutig, da sie stark von der Kombination aus Element und Ereignis abhängt. Und was die Sache noch verwirrender macht: 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 URL weitergeleitet wird, die in diesem Link angegeben ist.
In diesem Fall ist das Element ein Anker-Tag und das Ereignis ein Klickereignis. Für diese Kombination (<a>
+ click
) ist die Standardaktion festgelegt, dass das href-Attribut des Links aufgerufen wird. 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 kann. 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 Click-Event-Handler mit dem <a>
-Element verbunden und festgelegt, 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()
für diese Kombination verhindert, dass der Browser zu der URL kann, die im href-Attribut des<a>
-Elements angegeben ist.document
+ Ereignis „mousewheel“:preventDefault()
für diese Kombination verhindert das Scrollen mit dem Mausrad, das Scrollen mit der Tastatur funktioniert aber trotzdem.
↜ 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 solltest du jedoch vermeiden, da es dafür selten oder überhaupt einen gültigen 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 bei Weitem nicht 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? Es wird hell und hell! 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 würden (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 daher, dass click
-, keydown
-, mousedown
-, contextmenu
- und mousewheel
-Ereignisse an Elemente gelangen, die möglicherweise darauf warten. Außerdem rufen wir stopImmediatePropagation
auf, damit alle Handler, die nach diesem Schritt 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 hingehen 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 an einem Ort ansehen.
Danksagungen
Hero-Image von Tom Wilson auf Unsplash