JavaScript-Eventing im Detail

preventDefault und stopPropagation: Wann welche Methode verwendet werden sollte und was genau die einzelnen Methoden bewirken.

Event.stopPropagation() und Event.preventDefault()

Die JavaScript-Ereignisbehandlung ist oft unkompliziert. Das gilt insbesondere bei einer einfachen (relativ flachen) HTML-Struktur. Es wird jedoch etwas komplizierter, wenn Ereignisse durch eine Hierarchie von Elementen wandern (oder sich ausbreiten). In der Regel greifen Entwickler dann auf stopPropagation() und/oder preventDefault() zurück, um die Probleme zu beheben. Wenn Sie schon einmal gedacht haben: „Ich versuche es einfach mit preventDefault() und wenn das nicht funktioniert, versuche ich es mit stopPropagation() und wenn das nicht funktioniert, versuche ich es mit beidem“, dann ist dieser Artikel genau das Richtige für Sie. Ich erkläre Ihnen genau, was die einzelnen Methoden bewirken, wann Sie welche Methode verwenden sollten, und stelle Ihnen eine Reihe von funktionierenden Beispielen zur Verfügung. Mein Ziel ist es, deine Verwirrung ein für alle Mal zu beenden.

Bevor wir uns jedoch zu sehr ins Detail stürzen, ist es wichtig, kurz auf die beiden Arten der Ereignisbehandlung einzugehen, die in JavaScript möglich sind (in allen modernen Browsern – Internet Explorer vor Version 9 unterstützte das Erfassen von Ereignissen überhaupt nicht).

Eventing-Stile (Erfassung und Bubbling)

Alle modernen Browser unterstützen das Erfassen von Ereignissen, aber Entwickler nutzen es nur sehr selten. Interessanterweise war es die einzige Form von Ereignissen, die Netscape ursprünglich unterstützt hat. Der größte Rivale von Netscape, Microsoft Internet Explorer, unterstützte die Erfassung von Ereignissen überhaupt nicht, sondern nur eine andere Art von Ereignissen, das sogenannte Event Bubbling. Als das W3C gegründet wurde, erkannte man die Vorteile beider Eventing-Stile und erklärte, dass Browser beide über einen dritten Parameter für die Methode addEventListener unterstützen sollten. Ursprünglich war dieser Parameter nur ein boolescher Wert, aber alle modernen Browser unterstützen ein options-Objekt als dritten Parameter, mit dem Sie unter anderem angeben können, ob Sie die Ereigniserfassung verwenden möchten:

someElement.addEventListener('click', myClickHandler, { capture: true | false });

Das options-Objekt ist optional, ebenso wie das Attribut capture. Wenn eine der beiden Angaben fehlt, ist der Standardwert für capture false. Das bedeutet, dass Event Bubbling verwendet wird.

Erfassung von Ereignissen

Was bedeutet es, wenn Ihr Event-Handler „in der Capturing-Phase überwacht“ wird? Dazu müssen wir wissen, wie Ereignisse entstehen und wie sie übertragen werden. Das Folgende gilt für alle Ereignisse, auch wenn Sie als Entwickler sie nicht nutzen, sich nicht darum kümmern oder nicht daran denken.

Alle Ereignisse beginnen im Fenster und durchlaufen zuerst die Erfassungsphase. Wenn ein Ereignis ausgelöst wird, wird das Fenster also gestartet und bewegt sich zuerst „nach unten“ in Richtung des Zielelements. Das passiert auch, wenn du nur in der Bubbling-Phase zuhörst. Hier ein Beispiel für Markup und JavaScript:

<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 das Element #C klickt, wird ein Ereignis gesendet, das vom window stammt. Dieses Ereignis wird dann so an die untergeordneten Elemente weitergegeben:

window => document => <html> => <body> => usw., bis das Ziel erreicht ist.

Es spielt keine Rolle, ob für ein Click-Ereignis am Element window, document, <html> oder <body> (oder einem anderen Element auf dem Weg zum Ziel) ein Listener vorhanden ist. Ein Ereignis entsteht weiterhin am window und beginnt seinen Weg wie oben beschrieben.

In unserem Beispiel wird das Klickereignis dann weitergegeben. Das ist ein wichtiges Wort, da es direkt damit zusammenhängt, wie die Methode stopPropagation() funktioniert. Das wird später in diesem Dokument erläutert. Die Weitergabe erfolgt von window zum Zielelement (in diesem Fall #C) über jedes Element zwischen window und #C.

Das bedeutet, dass das Click-Event bei window beginnt und der Browser die folgenden Fragen stellt:

„Wird in der Capturing-Phase auf ein Click-Event für window gewartet?“ In diesem Fall werden die entsprechenden Event-Handler ausgelöst. In unserem Beispiel ist das nicht der Fall, daher werden keine Handler ausgelöst.

Als Nächstes wird das Ereignis an das document weitergegeben und der Browser fragt: „Wird in der Erfassungsphase auf dem document auf ein Klickereignis gewartet?“ In diesem Fall werden die entsprechenden Event-Handler ausgelöst.

Als Nächstes wird das Ereignis an das <html>-Element weitergegeben und der Browser fragt: „Wird in der Erfassungsphase auf das <html>-Element geklickt?“ In diesem Fall werden die entsprechenden Ereignishandler ausgelöst.

Als Nächstes wird das Ereignis an das <body>-Element weitergegeben und der Browser fragt: „Wird in der Erfassungsphase auf dem <body>-Element auf ein Klickereignis gewartet?“ In diesem Fall werden die entsprechenden Event-Handler ausgelöst.

Als Nächstes wird das Ereignis an das Element #A weitergegeben. Auch hier fragt der Browser: „Wird in der Capturing-Phase auf ein Click-Event für #A gewartet? Wenn ja, werden die entsprechenden Event-Handler 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: „Wird in der Capturing-Phase auf dem #C-Element auf ein Click-Ereignis gewartet?“ Die Antwort lautet dieses Mal „Ja“. Dieser kurze Zeitraum, in dem das Ereignis am Ziel ist, wird als „Zielphase“ bezeichnet. An diesem Punkt wird der Ereignishandler ausgelöst, der Browser gibt „#C was clicked“ (Auf #C wurde geklickt) in der Konsole aus und das war es, oder? Falsch! Wir sind noch lange nicht fertig. Der Prozess wird fortgesetzt, geht aber jetzt in die Bubbling-Phase über.

Event Bubbling

Der Browser fragt:

„Wird in der Bubbling-Phase auf ein Click-Event für #C gewartet?“ Achten Sie genau auf Folgendes. Es ist durchaus möglich, in beiden Phasen, der Capturing- und der Bubbling-Phase, auf Klicks (oder einen beliebigen Ereignistyp) zu warten. Wenn Sie in beiden Phasen Event-Handler eingerichtet haben (z.B. durch zweimaliges Aufrufen von .addEventListener(), einmal mit capture = true und einmal mit capture = false), werden beide Event-Handler für dasselbe Element ausgelöst. Es ist jedoch auch wichtig zu beachten, dass sie in verschiedenen Phasen ausgelöst werden (einer in der Erfassungsphase und einer in der Bubbling-Phase).

Als Nächstes wird das Ereignis an das zugehörige übergeordnete Element (häufiger als „Bubbling“ bezeichnet, da es so aussieht, als würde sich das Ereignis „aufwärts“ im DOM-Baum bewegen) #B weitergegeben und der Browser fragt: „Wird in der Bubbling-Phase auf #B auf Klickereignisse gewartet?“ In unserem Beispiel ist das nicht der Fall, daher werden keine Handler ausgelöst.

Als Nächstes wird das Ereignis an #A weitergeleitet und der Browser fragt: „Wird in der Bubbling-Phase auf #A auf Click-Ereignisse gewartet?“

Als Nächstes wird das Ereignis an <body> weitergeleitet: „Wird in der Bubbling-Phase auf Click-Ereignisse für das <body>-Element gewartet?“

Als Nächstes das <html>-Element: „Wird in der Bubbling-Phase auf Click-Events für das <html>-Element gewartet?

Als Nächstes wird document gefragt: „Wird in der Bubbling-Phase auf Click-Events für document gewartet?“

Schließlich window: „Wird in der Bubbling-Phase auf Click-Events im Fenster gewartet?“

Geschafft! Das war ein langer Weg und unser Ereignis ist wahrscheinlich sehr müde, aber ob du es glaubst oder nicht, das ist der Weg, den jedes Ereignis durchläuft. Meistens wird das nicht bemerkt, da Entwickler in der Regel nur an einer der beiden Ereignisphasen interessiert sind (und das ist in der Regel die Bubbling-Phase).

Es lohnt sich, etwas Zeit damit zu verbringen, mit dem Erfassen und Bubbling von Ereignissen zu experimentieren und einige Hinweise in der Konsole zu protokollieren, wenn Handler ausgelöst werden. Es ist sehr aufschlussreich, den Pfad zu sehen, den ein Ereignis nimmt. Hier ist ein Beispiel, in dem auf jedes Element in beiden Phasen gewartet 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, werden alle diese Event-Handler ausgelöst. Mit etwas CSS-Styling, um deutlicher zu machen, welches Element welches ist, sehen Sie hier die Konsolenausgabe des #C-Elements (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"

event.stopPropagation()

Nachdem wir nun wissen, woher Ereignisse stammen und wie sie sich sowohl in der Capturing- als auch in der Bubbling-Phase durch das DOM bewegen (d.h. weitergegeben werden), können wir uns event.stopPropagation() zuwenden.

Die Methode stopPropagation() kann für (die meisten) nativen DOM-Ereignisse aufgerufen werden. Ich sage „die meisten“, weil es einige gibt, bei denen der Aufruf dieser Methode nichts bewirkt, da das Ereignis ohnehin nicht weitergegeben wird. Ereignisse wie focus, blur, load, scroll und einige andere fallen in diese Kategorie. Sie können stopPropagation() anrufen, aber es wird nichts Interessantes passieren, da diese Ereignisse nicht weitergegeben werden.

Was macht stopPropagation?

Es tut im Grunde genau das, was es verspricht. Wenn Sie die Methode aufrufen, wird das Ereignis ab diesem Zeitpunkt nicht mehr an Elemente weitergegeben, an die es sonst weitergeleitet würde. Das gilt in beide Richtungen (Erfassung und Bubble-up). Wenn Sie stopPropagation() also irgendwo in der Erfassungsphase aufrufen, erreicht das Ereignis nie die Ziel- oder Bubbling-Phase. Wenn Sie die Methode in der Bubbling-Phase aufrufen, hat das Ereignis die Capturing-Phase bereits durchlaufen. Es wird jedoch ab dem Punkt, an dem Sie die Methode aufgerufen haben, nicht mehr „nach oben weitergeleitet“.

Was würde passieren, wenn wir in unserem Beispiel-Markup stopPropagation() in der Erfassungsphase für das Element #B aufrufen?

Dies 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"

Wie wäre es, wenn wir die Weitergabe in der Bubbling-Phase bei #A 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"

Noch ein Beispiel, nur zum Spaß. Was passiert, wenn wir stopPropagation() in der Zielphase für #C aufrufen? Die „Zielphase“ ist der Zeitraum, in dem das Ereignis am Ziel ist. Dies 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 Event-Handler für #C, in dem wir „click on #C in the capturing phase“ protokollieren, wird weiterhin ausgeführt, der Event-Handler, in dem wir „click on #C in the bubbling phase“ protokollieren, jedoch nicht. Das sollte einleuchten. Wir haben stopPropagation() aus dem ersten aufgerufen. An diesem Punkt wird die Weitergabe des Ereignisses also beendet.

Ich möchte Sie ermutigen, in diesen Live-Demos selbst aktiv zu werden. Klicken Sie nur auf das #A-Element oder nur auf das body-Element. Versuche, vorherzusagen, was passieren wird, und beobachte dann, ob du richtig liegst. An diesem Punkt sollten Sie ziemlich genaue Vorhersagen treffen können.

event.stopImmediatePropagation()

Was ist das für eine seltsame und selten verwendete Methode? Sie ähnelt stopPropagation, aber anstatt zu verhindern, dass ein Ereignis an Nachfolger (Erfassung) oder Vorgänger (Bubbling) weitergeleitet wird, wird diese Methode nur angewendet, wenn Sie mehr als einen Ereignishandler mit einem einzelnen Element verbunden haben. Da addEventListener() ein Multicast-Ereignissystem unterstützt, ist es durchaus möglich, einen Ereignishandler mehr als einmal mit einem einzelnen Element zu verknüpfen. In den meisten Browsern werden Ereignishandler in der Reihenfolge ausgeführt, in der sie eingerichtet wurden. Durch den Aufruf 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 zu folgender Konsolenausgabe:

"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 im zweiten Ereignis-Handler e.stopImmediatePropagation() aufgerufen wird. Wenn wir stattdessen e.stopPropagation() aufgerufen hätten, wäre der dritte Handler trotzdem ausgeführt worden.

event.preventDefault()

Wenn stopPropagation() verhindert, dass ein Ereignis „nach unten“ (Erfassung) oder „nach oben“ (Bubbling) übertragen wird, was macht dann preventDefault()? Es scheint etwas Ähnliches zu tun. Ist das

Nicht wirklich. Die beiden Begriffe werden oft verwechselt, haben aber eigentlich nicht viel miteinander zu tun. Wenn Sie preventDefault() sehen, fügen Sie in Gedanken das Wort „Aktion“ hinzu. Denken Sie an „Standardaktion verhindern“.

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 die Verwirrung noch zu steigern, gibt es manchmal gar keine Standardaktion.

Beginnen wir mit einem sehr einfachen Beispiel. Was passiert Ihrer Meinung nach, wenn Sie auf einer Webseite auf einen Link klicken? Sie erwarten natürlich, dass der Browser die URL aufruft, die in diesem Link angegeben ist. 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 zu navigieren. Was, wenn Sie verhindern möchten, dass der Browser diese Standardaktion ausführt? Angenommen, Sie möchten verhindern, dass der Browser zur URL navigiert, die durch das href-Attribut des <a>-Elements angegeben wird. Das ist es, 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,
);

Normalerweise würde ein Klick auf den Link mit der Bezeichnung „The Avett Brothers“ dazu führen, dass www.theavettbrothers.com aufgerufen wird. In diesem Fall haben wir jedoch einen Click-Event-Handler für das <a>-Element eingerichtet und angegeben, dass die Standardaktion verhindert werden soll. Wenn ein Nutzer auf diesen Link klickt, wird er also nicht weitergeleitet, sondern in der Konsole wird einfach „Maybe we should just play some of their music right here instead?“ (Vielleicht sollten wir einfach hier Musik abspielen?) protokolliert.

Welche anderen Kombinationen aus Elementen und Ereignissen ermöglichen es Ihnen, die Standardaktion zu verhindern? Ich kann sie unmöglich alle auflisten. Manchmal musst du einfach experimentieren. Hier einige Beispiele:

  • <form>-Element + „submit“-Ereignis: preventDefault() für diese Kombination verhindert das Senden eines Formulars. Das ist nützlich, wenn Sie eine Validierung durchführen möchten. Sollte etwas fehlschlagen, können Sie „preventDefault“ bedingt aufrufen, um das Senden des Formulars zu verhindern.

  • <a>-Element + „click“-Ereignis: preventDefault() für diese Kombination verhindert, dass der Browser zur URL navigiert, die im href-Attribut des <a>-Elements angegeben ist.

  • document + „mousewheel“-Ereignis: preventDefault() für diese Kombination verhindert das Scrollen der Seite mit dem Mausrad. Das Scrollen mit der Tastatur ist jedoch weiterhin möglich.
    ↜ Dazu muss addEventListener() 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, Tabben und Hervorheben mit der Tastatur nicht mehr möglich ist.

  • document + „mousedown“-Ereignis: preventDefault() für diese Kombination verhindert das Markieren von Text mit der Maus und alle anderen „Standard“-Aktionen, die mit einem Mausklick ausgelöst werden.

  • <input>-Element + „keypress“-Ereignis: preventDefault() für diese Kombination verhindert, dass vom Nutzer eingegebene Zeichen das Eingabeelement erreichen. Das sollte aber nicht passieren, da es selten bis nie einen gültigen Grund dafür gibt.

  • document + „contextmenu“-Ereignis: preventDefault() für diese Kombination verhindert, dass das native Browserkontextmenü angezeigt wird, wenn ein Nutzer mit der rechten Maustaste klickt oder lange auf das Display drückt (oder auf andere Weise ein Kontextmenü aufruft).

Diese Liste ist nicht vollständig, soll Ihnen aber eine gute Vorstellung davon vermitteln, wie preventDefault() verwendet werden kann.

Ein lustiger Streich?

Was passiert, wenn Sie in der Erfassungsphase stopPropagation() und preventDefault(), beginnend mit dem Dokument? Es wird lustig! Das folgende Code-Snippet macht jede Webseite so gut wie 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 sollten (außer vielleicht, um jemandem einen Streich zu spielen), aber es ist nützlich, darüber nachzudenken, was hier passiert, und zu verstehen, warum dadurch die entsprechende Situation entsteht.

Alle Ereignisse stammen von window. In diesem Snippet verhindern wir also, dass click-, keydown-, mousedown-, contextmenu- und mousewheel-Ereignisse jemals Elemente erreichen, die auf sie warten. Wir rufen auch stopImmediatePropagation auf, 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 lediglich, dass Ereignisse dorthin gelangen, wo sie sonst hingelangen würden.

Wir rufen aber auch preventDefault() auf, wodurch die Standardaktion verhindert wird. Alle Standardaktionen wie Mausrad-Scrollen, Tastatur-Scrollen, Markieren, Tabben, Link-Klicken, Kontextmenü-Anzeige usw. werden verhindert, sodass die Seite in einem ziemlich nutzlosen Zustand verbleibt.

Danksagungen

Hero-Image von Tom Wilson auf Unsplash.