Die Gefahren des Stoppens der Event-Propagation

Avatar of Philip Walton
Philip Walton am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Der folgende Beitrag ist ein Gastbeitrag von Philip Walton (@philwalton). Er wird erklären, warum das Stoppen der Event-Propagation nichts ist, was man leichtfertig tun sollte, und wahrscheinlich etwas, das man ganz vermeiden sollte.

Wenn Sie ein Frontend-Entwickler sind, mussten Sie wahrscheinlich irgendwann in Ihrer Karriere ein Popup oder Dialogfeld erstellen, das sich selbst schloss, nachdem der Benutzer irgendwo anders auf der Seite geklickt hatte. Wenn Sie online nach dem besten Weg gesucht haben, dies zu tun, sind Sie wahrscheinlich auf diese Stack Overflow-Frage gestoßen: Wie erkennt man einen Klick außerhalb eines Elements?.

Hier ist, was die am besten bewertete Antwort empfiehlt

$('html').click(function() {
  // Hide the menus if visible.
});

$('#menucontainer').click(function(event){
  event.stopPropagation();
});

Falls nicht klar ist, was dieser Code tut, hier eine kurze Zusammenfassung: Wenn ein Klick-Ereignis zum ``-Element propagiert, werden die Menüs ausgeblendet. Wenn das Klick-Ereignis von innerhalb von `#menucontainer` ausging, stoppen Sie dieses Ereignis, damit es niemals das ``-Element erreicht, wodurch nur Klicks außerhalb von `#menucontainer` die Menüs ausblenden.

Der obige Code ist einfach, elegant und clever zugleich. Doch leider ist er absolut schrecklicher Rat.

Diese Lösung ist grob äquivalent zur Behebung einer undichten Dusche, indem man das Wasser im Badezimmer abdreht. Es funktioniert, ignoriert aber die Möglichkeit, dass anderer Code auf der Seite dieses Ereignis wissen muss.

Dennoch ist es die am höchsten bewertete Antwort auf diese Frage, also gehen die Leute davon aus, dass es ein fundierter Rat ist.

Was kann schiefgehen?

Sie denken sich vielleicht: Wer schreibt denn heute noch so etwas selbst? Ich benutze eine gut getestete Bibliothek wie Bootstrap, also muss ich mir keine Sorgen machen, oder?

Leider nein. Das Stoppen der Event-Propagation ist nicht nur etwas, das von schlechten Stack Overflow-Antworten empfohlen wird; es findet sich auch in einigen der beliebtesten Bibliotheken, die heute verwendet werden.

Um dies zu beweisen, zeige ich Ihnen, wie einfach es ist, einen Fehler zu erzeugen, indem Sie Bootstrap in einer Ruby on Rails-App verwenden. Rails wird mit einer JavaScript-Bibliothek namens jquery-ujs geliefert, die es Entwicklern ermöglicht, per Deklaration entfernte AJAX-Aufrufe an Links über das Attribut `data-remote` hinzuzufügen.

Im folgenden Beispiel sollte sich das Dropdown schließen, wenn Sie es öffnen und dann irgendwo anders im Frame klicken. Wenn Sie jedoch das Dropdown öffnen und dann auf "Remote Link" klicken, funktioniert es nicht.

Siehe den Pen Stop Propagation Demo von Philip Walton (@philwalton) auf CodePen.

Dieser Fehler tritt auf, weil der Bootstrap-Code, der für das Schließen des Dropdown-Menüs zuständig ist, auf Klick-Ereignisse im Dokument hört. Da jquery-ujs jedoch die Event-Propagation in seinen `data-remote`-Link-Handlern stoppt, erreichen diese Klicks nie das Dokument, und somit wird der Bootstrap-Code nie ausgeführt.

Das Schlimmste an diesem Fehler ist, dass Bootstrap (oder jede andere Bibliothek) absolut nichts tun kann, um ihn zu verhindern. Wenn Sie mit dem DOM arbeiten, sind Sie immer den Launen jedes anderen schlecht geschriebenen Codes auf der Seite ausgeliefert.

Das Problem mit Events

Wie viele Dinge in JavaScript sind DOM-Events global. Und wie die meisten Leute wissen, können globale Variablen zu unübersichtlichem, gekoppeltem Code führen.

Die Modifikation eines einzelnen, flüchtigen Ereignisses mag zunächst harmlos erscheinen, birgt aber Risiken. Wenn Sie das Verhalten ändern, das die Leute erwarten und auf das anderer Code angewiesen ist, werden Sie Fehler haben. Das ist nur eine Frage der Zeit.

Und meiner Erfahrung nach sind diese Art von Fehlern einige der am schwierigsten zu findenden.

Warum stoppen Leute die Event-Propagation?

Wir wissen, dass es schlechte Ratschläge im Internet gibt, die die unnötige Verwendung von `stopPropagation` fördern, aber das ist nicht der einzige Grund, warum Leute es tun.

Häufig stoppen Entwickler die Event-Propagation, ohne es überhaupt zu merken.

Return false

Es gibt viel Verwirrung darüber, was passiert, wenn Sie aus einem Event-Handler `false` zurückgeben. Betrachten Sie die folgenden drei Fälle

<!-- An inline event handler. -->
<a href="http://google.com" onclick="return false">Google</a>
// A jQuery event handler.
$('a').on('click', function() {
  return false;
});
// A native event handler.
var link = document.querySelector('a');

link.addEventListener('click', function() {
  return false;
});

Diese drei Beispiele scheinen alle genau dasselbe zu tun (nur `false` zurückzugeben), aber in Wirklichkeit sind die Ergebnisse ganz anders. Hier ist, was in jedem der obigen Fälle tatsächlich passiert

  1. Das Zurückgeben von `false` aus einem Inline-Event-Handler verhindert, dass der Browser zur Linkadresse navigiert, stoppt aber nicht die Propagation des Ereignisses durch das DOM.
  2. Das Zurückgeben von `false` aus einem jQuery-Event-Handler verhindert die Navigation des Browsers zur Linkadresse **und** stoppt die Propagation des Ereignisses durch das DOM.
  3. Das Zurückgeben von `false` aus einem regulären DOM-Event-Handler tut absolut nichts.

Wenn Sie erwarten, dass etwas passiert, und es nicht tut, ist das verwirrend, aber Sie erwischen es normalerweise sofort. Ein viel größeres Problem ist, wenn Sie erwarten, dass etwas passiert, und es *tut* es, aber mit unvorhergesehenen und unbemerkten Nebenwirkungen. Von dort kommen Albtraum-Fehler.

Im jQuery-Beispiel ist es überhaupt nicht klar, dass das Zurückgeben von `false` anders funktioniert als die anderen beiden Event-Handler, aber das tut es. Unter der Haube ruft jQuery tatsächlich die folgenden beiden Anweisungen auf

event.preventDefault();
event.stopPropagation();

Wegen der Verwirrung um `return false` und der Tatsache, dass es in jQuery-Handlern die Event-Propagation stoppt, empfehle ich, es niemals zu verwenden. Es ist viel besser, Ihre Absichten explizit zu machen und diese Event-Methoden direkt aufzurufen.

Hinweis: Wenn Sie jQuery mit CoffeeScript verwenden (das automatisch den letzten Ausdruck einer Funktion zurückgibt), stellen Sie sicher, dass Sie Ihre Event-Handler nicht mit etwas beenden, das zu dem booleschen Wert `false` ausgewertet wird, sonst haben Sie dasselbe Problem.

Performance

Immer wieder liest man Ratschläge (meist vor einiger Zeit geschrieben), die aus Performance-Gründen die Propagation stoppen empfehlen.

In den Tagen von IE6 und noch älteren Browsern konnte ein kompliziertes DOM Ihre Website wirklich verlangsamen. Und da Ereignisse durch das gesamte DOM laufen, galt: Je mehr Knoten Sie hatten, desto langsamer wurde alles.

Peter Paul Koch von quirksmode.org empfahl diese Praxis in einem alten Artikel zu diesem Thema

Wenn Ihre Dokumentstruktur sehr komplex ist (viele verschachtelte Tabellen und dergleichen), können Sie Systemressourcen sparen, indem Sie das Bubbling ausschalten. Der Browser muss jedes einzelne Elternelement des Event-Ziels durchlaufen, um zu sehen, ob es einen Event-Handler hat. Selbst wenn keine gefunden werden, kostet die Suche trotzdem Zeit.

Mit den heutigen modernen Browsern werden Sie jedoch von den Performance-Gewinnen, die Sie durch das Stoppen der Event-Propagation erzielen, wahrscheinlich nichts bemerken. Es ist eine Mikrooptimierung und sicherlich nicht Ihr Performance-Engpass.

Ich empfehle, sich keine Gedanken darüber zu machen, dass Ereignisse durch das gesamte DOM propagieren. Es ist schließlich Teil der Spezifikation, und Browser sind darin sehr gut geworden.

Was stattdessen tun?

Als allgemeine Regel sollte das Stoppen der Event-Propagation niemals eine Lösung für ein Problem sein. Wenn Sie eine Website mit mehreren Event-Handlern haben, die sich manchmal gegenseitig stören, und Sie feststellen, dass das Stoppen der Propagation alles zum Laufen bringt, ist das schlecht. Es mag Ihr unmittelbares Problem lösen, aber es schafft wahrscheinlich ein weiteres, das Sie nicht kennen.

Das Stoppen der Propagation sollte als Abbrechen eines Ereignisses betrachtet werden und nur mit dieser Absicht verwendet werden. Vielleicht möchten Sie die Formularübermittlung verhindern oder den Fokus von einem Bereich der Seite abhalten. In diesen Fällen stoppen Sie die Propagation, weil Sie nicht möchten, dass ein Ereignis eintritt, nicht weil Sie einen unerwünschten Event-Handler höher im DOM registriert haben.

Im obigen Beispiel "Wie erkennt man einen Klick außerhalb eines Elements?" ist der Zweck des Aufrufs von `stopPropagation` nicht, das Klick-Ereignis vollständig zu eliminieren, sondern die Ausführung anderer Codes auf der Seite zu vermeiden.

Dies ist nicht nur eine schlechte Idee, weil sie das globale Verhalten verändert, sondern auch, weil sie die Logik zum Ausblenden von Menüs an zwei verschiedenen und nicht zusammenhängenden Stellen platziert, was sie viel anfälliger macht als nötig.

Eine viel bessere Lösung ist es, einen einzigen Event-Handler zu haben, dessen Logik vollständig gekapselt ist und dessen alleinige Verantwortung darin besteht, festzustellen, ob das Menü für das gegebene Ereignis ausgeblendet werden soll oder nicht.

Wie sich herausstellt, erfordert diese bessere Option auch weniger Code

$(document).on('click', function(event) {
  if (!$(event.target).closest('#menucontainer').length) {
    // Hide the menus.
  }
});

Der obige Handler hört auf Klicks auf das Dokument und prüft, ob das Ereignisziel `#menucontainer` ist oder `#menucontainer` als Elternteil hat. Wenn nicht, wissen Sie, dass der Klick von außerhalb von `#menucontainer` ausging, und können daher die Menüs ausblenden, wenn sie sichtbar sind.

Default Prevented?

Vor etwa einem Jahr begann ich mit dem Schreiben einer Bibliothek zur Event-Behandlung, um bei diesem Problem zu helfen. Anstatt die Event-Propagation zu stoppen, würde man ein Ereignis einfach als "behandelt" markieren. Dies würde es Event-Listenern, die weiter oben im DOM registriert sind, ermöglichen, ein Ereignis zu inspizieren und basierend darauf, ob es "behandelt" wurde oder nicht, zu bestimmen, ob weitere Maßnahmen erforderlich sind. Die Idee war, dass man "Event-Propagation stoppen" könnte, ohne sie tatsächlich zu stoppen.

Wie sich herausstellte, benötigte ich diese Bibliothek nie. In 100 % der Fälle, in denen ich prüfen wollte, ob ein Ereignis "behandelt" wurde, bemerkte ich, dass ein vorheriger Listener `preventDefault` aufgerufen hatte. Und die DOM-API bietet bereits eine Möglichkeit, dies zu inspizieren: die Eigenschaft `defaultPrevented`.

Um dies zu verdeutlichen, gebe ich ein Beispiel.

Stellen Sie sich vor, Sie fügen dem Dokument einen Event-Listener hinzu, der Google Analytics verwendet, um zu verfolgen, wann Benutzer auf Links zu externen Domänen klicken. Es könnte ungefähr so aussehen

$(document).on('click', 'a', function(event) {
  if (this.hostname != 'css-tricks.com') {
    ga('send', 'event', 'Outbound Link', this.href);
  }
});

Das Problem bei diesem Code ist, dass nicht alle Link-Klicks zu anderen Seiten führen. Manchmal fängt JavaScript den Klick ab, ruft `preventDefault` auf und tut etwas anderes. Die oben beschriebenen `data-remote`-Links sind ein Paradebeispiel dafür. Ein weiteres Beispiel ist ein Twitter-Share-Button, der ein Popup öffnet, anstatt zu twitter.com zu gehen.

Um solche Klicks zu vermeiden, mag es verlockend sein, die Event-Propagation zu stoppen, aber die Überprüfung des Ereignisses auf `defaultPrevented` ist ein viel besserer Weg.

$(document).on('click', 'a', function(event) {

  // Ignore this event if preventDefault has been called.
  if (event.defaultPrevented) return;

  if (this.hostname != 'css-tricks.com') {
    ga('send', 'event', 'Outbound Link', this.href);
  }
});

Da das Aufrufen von `preventDefault` in einem Click-Handler immer verhindert, dass der Browser zu einer Linkadresse navigiert, können Sie zu 100 % sicher sein, dass der Benutzer nirgendwo hingegangen ist, wenn `defaultPrevented` true ist. Mit anderen Worten, diese Technik ist sowohl zuverlässiger als `stopPropagation` als auch frei von Nebenwirkungen.

Fazit

Hoffentlich hat dieser Artikel Ihnen geholfen, DOM-Ereignisse mit neuen Augen zu sehen. Sie sind keine isolierten Stücke, die ohne Konsequenzen modifiziert werden können. Sie sind globale, miteinander verbundene Objekte, die oft weitaus mehr Code beeinflussen, als man zunächst denkt.

Um Fehler zu vermeiden, ist es fast immer am besten, Ereignisse in Ruhe zu lassen und sie so propagieren zu lassen, wie es der Browser vorsieht.

Wenn Sie sich jemals unsicher sind, was zu tun ist, stellen Sie sich folgende Frage: Ist es möglich, dass anderer Code, entweder jetzt oder in Zukunft, wissen möchte, dass dieses Ereignis stattgefunden hat? Die Antwort ist normalerweise ja. Ob für etwas so Triviales wie ein Bootstrap-Modal oder so Kritischem wie Event-Tracking-Analysen, der Zugriff auf Ereignisobjekte ist wichtig. Im Zweifelsfall nicht die Propagation stoppen.