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
- 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.
- 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.
- 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.
Der zusätzliche Vorteil dieser Lösung ist, dass `#menucontainer` zum Zeitpunkt der Bindung nicht im DOM vorhanden sein muss. Im Gegensatz zur vorgeschlagenen Antwort in Stackoverflow.
Das Hauptzitat
„Wenn Sie das Verhalten ändern, das die Leute erwarten und auf das anderer Code angewiesen ist, werden Sie Fehler haben.“
Nicht „können“, nicht „möglicherweise“, sondern „werden“. Unentdeckte Fehler sind immer noch Fehler.
Philip, vergisst du nicht, „event“ an die letzten beiden jQuery-Funktionsbeispiele zu übergeben?
Ja, Sie haben Recht. Danke, dass Sie den Fehler bemerkt haben.
Danke Philip, dass Sie den Fehler mit „event“ aktualisiert haben
„event“ ist eine globale Variable, die „undefined“ ist, es sei denn, sie befindet sich innerhalb eines Handlers.
Auch hier hat prototype.js alles, was Sie brauchen.
Ihr Klick-Event-Handler muss das Element bestimmen, das den Klick empfangen hat. Nehmen wir an, es ist in der Variable 'obj' gespeichert. Dann gehen Sie einfach nach oben
Prototype.js hat eine vollständige Lösung dafür: `Event.findElement()`.
Natürlich würden Sie in einem allgemeineren Event-Handler wahrscheinlich eine virtuelle CSS-Klasse für alle Objekte verwenden, die ausgeblendet werden müssen, wenn außerhalb geklickt wird, z. B. selbst erstellte Dropdown-Boxen. Das ist schnell genug, selbst für `mousemove`-Events, und es ist bis IE6 kompatibel (wenn Sie es wirklich müssen...). Prototype.js bietet ein `mouseleave`-Polyfill, aber die Ansätze `up()` und `findElement()` sind vielseitiger.
Dies kann nur ausreichend schnell sein, weil die meisten JS-Engines die Prototype-Erweiterungen (die `up()`-Methode in diesem Fall) vorkompilieren. Wenn Sie das DOM jedes Mal "manuell" parsen müssten, wenn ein Ereignis auftritt, wäre es viel langsamer. Und **das** ist einer der Hauptgründe, warum sich JS-Programmierer mit der Philosophie der Prototype-Erweiterungen vertraut machen und klassen- oder funktionsbasierte Ansätze aufgeben sollten. Sie brauchen Prototype.js nicht nur für diese Lösung; Die `up()`-Prototype-Erweiterung benötigt nur ein paar Codezeilen, wenn Sie sie selbst implementieren müssen.
Ach, nun, ich bin sicher, es gibt eine ähnliche Lösung mit JQuery auch...
Das ist mein reines JavaScript-Äquivalent für den jQuery-Code
Ist das gut?
@Valtteri: Nein, Sie haben es falsch herum verstanden – das Menü sollte ausgeblendet werden, wenn der Klick *außerhalb* von `#menucontainer` stattfand – mit `menucontainer.contains(event.target)` überprüfen Sie jedoch, ob das Ziel des Ereignisses ein Nachkomme von `#menucontainer` ist.
Ich habe auch etwas Ähnliches wie die ursprüngliche Stackoverflow-Lösung verwendet, die ich elegant fand, bis ich auf dasselbe Problem stieß, dass Drittanbieter-Code die Propagation stoppte und somit Dropdowns offen ließ.
Wenn wir jedoch IE8 und älter ignorieren können, gibt es eine Problemumgehung für Drittanbieter-Code, der mit Ereignissen herumspielt. Wir können *Event-Capturing* verwenden, um das Ereignis auf Dokumentebene zu verarbeiten, bevor irgendein anderes Element es empfängt.
Durch die Übergabe von `true` als drittem Parameter wird der Event-Handler an die Capturing-Phase gebunden. Capturing bedeutet, dass das Ereignis zunächst von dem Dokument bis zum Event-Ziel seinen Weg findet, vor der Bubbling-Phase, die allgemein bekannt ist, vom Ziel zurück zum Dokument. Die Reihenfolge von Capturing und Bubbling wird im oben genannten Quirksmode-Artikel erklärt.
Es gibt eine umständliche Methode sogar für IE8 und älter: Lauschen Sie auf `mouseup` statt auf `click`, da dies ein winzig kleines bisschen früher ausgelöst wird...
Die beste Lösung ist jedoch immer noch, das zu tun, was der Artikel sagt: Stoppen Sie die Propagation nicht.
Sie können ein transparentes Overlay verwenden, um Benutzern das Klicken außerhalb des Modals zum Schließen zu ermöglichen, ohne die Event-Propagation stoppen zu müssen.
Demo: http://codepen.io/anon/pen/Iicae
Hier denkt der Benutzer, dass er Teile des Dokuments außerhalb des Modals anklickt. Tatsächlich klickt er auf das Element `.overlay`, das die gesamte Seite abdeckt.
Hmph. Das Markdown ist kaputt, oder liegt es an `strip_tags()` in Kommentaren :(
Der leere Selektor oben sollte `$(‘<div class=”overlay”></div>’)` sein
Ja, transparente Overlays sind *gute Praxis*, aber sie haben ein Problem. Z-Index. Wenn Sie lokale Z-Indizes verwenden möchten, um zu verhindern, dass sie zu groß werden, würde das schaden. Oder Sie sollten sicherstellen, dass keines der Elternelemente der transparenten Overlays (insbesondere, wenn Sie es nicht beabsichtigen) lokal positioniert und mit Z-Index versehen ist.
Ich verwende das jQuery `focusout`-Event, um ein Formular auszublenden, und es funktioniert einwandfrei, oder übersehe ich etwas?
Ich verwende immer den folgenden Code, um dieses Verhalten zu erreichen
@CBroe: Ja, ich meine diesen Code
(mit den echten Ampersands)
Ich habe es immer abgelehnt, die Events auf dem Dokument zu verwenden, um ein anderes geöffnetes Element zu benachrichtigen.
Ähnlich wie Kadhim habe ich immer ein `blur`-Event auf dem Menü verwendet. Dann ist Bubbling kein Problem.
Ich habe mich über die Unterscheidung von `return false` zwischen Inline-JS, jQuery und traditionellem JS gefragt. Danke dafür und für den Rest des Artikels.
Sehr schöner Artikel! Da Sie mit dem Erkennen von Klicks außerhalb eines Elements beginnen, würde ich Folgendes empfehlen: http://bassta.bg/2013/08/detect-click-event-outside-element/
var $box = $(“.box”);
Absolut sicher, und Sie stoppen die Event-Propagation nicht. Wenn Sie mit anderen Leuten zusammenarbeiten, müssen Sie sicher sein, dass Sie keine Ereignisse stoppen, die propagieren/bubbeln.
Sehr schöner Artikel. Alle Instanzen von StopPropagation aus meinem Projekt entfernt. https://github.com/dekajp/google-closure-grid
Obwohl ich im Grunde mit Ihrer Prämisse übereinstimme, dass man das Event-Bubbling nicht willkürlich verhindern sollte, muss ich Ihrer Anregung widersprechen, dass es besser ist, ein Spinnennetz aus Ausnahmebehandlungscode anstelle dessen zu erstellen. Auch Ihre 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.“ ist ungenau. Die Antwort ist nicht „normalerweise“ ja, sondern „kann sein“ ja.
Ich spreche aus Erfahrung, da ich gerade einen Tag damit verbracht habe, Code umzuschreiben, weg von dem, was Sie vorschlagen. Es gab 4 verschiedene Menüs/Popups, die sich schließen mussten, wenn der Benutzer außerhalb von ihnen klickte (was übrigens das einzige war, was sie tun mussten, und kein anderer Code ihren Zustand wissen musste). Beim Schreiben eines globalen, immer aktiven Listeners wurde ich gezwungen, buchstäblich Dutzende und Aberdutzende von Ausnahmen für all die anderen Klicks zu schreiben, die etwas anderes tun mussten. Ich denke, es ist viel sinnvoller, auf den „Schließ-Klick“ zu hören, nur wenn das Menü/Popup geöffnet wurde.
„Sieht so aus, als hätten Sie das Menü geöffnet – lassen Sie mich einfach wissen, wenn Sie damit fertig sind“ ist VIEL sinnvoller als „Sie haben irgendwo auf der Seite geklickt! Haben Sie auf das hier geklickt? Nein. War es das hier? Nein. War es das hier? Nein. War es DAS? Nein. Wie wäre es hiermit? Nein. Das? Nein. Das? Nein. usw. usw. usw.“