Orlok: Event Handling und Event Delegation

Beitrag lesen

Hallo

Kann JQuery keine ich sag mal "zur laufzeit" erstellten elemente ansprechen?

Doch, es kann, in einem gewissen Sinne. Es kann nur nicht zaubern, sondern muss sich an die zugrundeliegenden Möglichkeiten und Gegebenheiten halten. […]

This.

Wann wurde hier eigenlich zuletzt darüber diskutiert, wie sinnvoll es ist jQuery zu verwenden, wenn man JavaScript nicht beherrscht? ;-)

Ich meine mich dunkel daran erinnern zu können, hierzu mal einen schönen Vergleich gelesen zu haben, frei nach Camping_RIDER:

„jQuery verwenden ohne JavaScript zu können ist wie einen Rennwagen zu fahren ohne einen Führerschein zu haben.“

Jedenfalls ist die Geschichte mit den Events eigentlich nicht wirklich schwer zu verstehen, wenn man sich die Mühe macht, sich wenigstens etwas in die Materie einzulesen…

Event Handling

Fangen wir mal damit an, was HTML-Elemente eigentlich sind, nämlich in erster Linie Objekte in der internen Repräsentation des Document Object Models, welche der Browser auf Grundlage des HTML-Dokumentes und der eingebundenen Skripte erstellt.

Diese Objekte, welche also die Elemente repräsentieren, verfügen nun über eine Schnittstelle Event Target, die es unter anderem erlaubt, Event Listener hinzuzufügen und wieder zu löschen:

element.addEventListener('event', callback, capture);

element.removeEventListener('event', callback, capture);

Das heißt, jedes Element hat eine assoziierte Liste der für dieses Element registrierten Event Listener.

Wobei zu beachten ist, dass keine identischen Event Listener für ein Element existieren dürfen, sprich, wenn man für das gleiche Element mehr als einen Event Listener mit den selben Argumenten registriert, dann wird der bestehende Listeneintrag durch die nachfolgenden überschrieben. Aber das nur nebenbei.

Jedenfalls dürfte dadurch klar werden, dass man keine Event Handler für DOM-Objekte registrieren kann, die zum Zeitpunkt dieser Operation noch gar nicht existieren…

Aber weiter im Text: Was sind eigentlich Events?

Events sind ebenfalls Objekte, also Container für Eigenschaften und Methoden, auf die über die Schnittstelle Event zugegriffen werden kann.

Dies ist möglich, da das Event einem assoziierten Event Handler als Parameter übergeben wird:

var callback = function (e) {
  var event = e;
};

element.addEventListener('click', callback);

// oder

element.addEventListener('click', function (e) {
  var event = e;
});

Zu den Eigenschaften gehört hier nun in erster Linie der Typ des Events, welcher den Event Listenern als steuernder Eingabeparameter übergeben wird, und der wiefolgt ausgelesen werden kann:

element.addEventListener('click', function (e) {
  var eventType = e.type;  // 'click'
});

Weiterhin gehört zu den Attributen eines Events das Zielobjekt, also das DOM-Objekt, an welches das Event verschickt wird:

document.addEventListener('click', function (e) {
  var targetElement = e.target;
});

Darüber hinaus gibt es auch noch eine Eigenschaft, in welcher das DOM-Objekt, also in der Regel das Element hinterlegt ist, dessen Event Listener tatsächlich aufgerufen wurde, und welches sich von dem eigentlichen Zielobjekt, wie wir gleich noch sehen werden, unterscheiden kann:

element.addEventListener('click', function (e) {
  var thisElement = e.currentTarget;
});

Hierbei sei noch angemerkt, dass der Wert von e.currentTarget beim Aufruf der Callback-Funktion als deren this-Wert fungiert, das Element, für welches der Event Listener registriert wurde, also über this referenziert werden kann:

element.addEventListener('click', function (e) {
  var thisElement = this;   // this = e.currentTarget
});

Abgesehen von den bereits genannten Event-Attributen, gibt es natürlich noch einige Eigenschaften und Methoden mehr, deren Verständnis allerdings voraussetzt, dass man verstanden hat, wie die Event-Objekte überhaupt zu ihren Zielobjekten gelangen…

Es ist nämlich keinesfalls so, dass die Events nur an dasjenige DOM-Objekt übergeben werden, welches als event.target hinterlegt ist, sondern im Gegenteil ist es so, dass die Events über das globale Objekt in die Struktur des DOM eingespeist werden, das heißt in dem Fall, dass es sich bei dem User Agent um einen Browser handelt, ist Window die erste Station des Events.

Wenn ein Event versandt wird, dann wird intern ein sogenannter Eventpfad festgelegt, der, ausgehend vom Zielelement, also dem Event Target, bis hoch zum globalen Window-Objekt alle Vorfahrenelemente von target umfasst.

Beispiel:

<body>
  <header>
    <h1> Hallo Welt </h1>
  </header>
</body>

… und …

var heading = document.getElementsByTagName('h1')[0];

heading.addEventListener('click', function (e) {
  // ...
});

Hier ist nun ein Event Listener für das H1-Element registriert, und wenn jetzt auf dieses Element geklickt wird, dann sieht der Eventpfad des 'click'-Events zunächst so aus:

| window | document | body | header | h1 |

Das Event-Objekt wird also von jeder Station an die jeweils nächste entlang des Pfades weitergereicht.

Dabei ist im Übrigen zu beachten, dass der Event Path bereits zum Zeitpunkt des Versands (event dispatch) festgelegt wird, das heißt, Änderungen im DOM, die nach dem Feuern des Events vorgenommen werden, haben auf den Weg des Events keinen Einfluss.

Diese erste Phase, die jedes Event durchläuft, nennt man CAPTURING-PHASE, und sie dauert, so man sie nicht unterbricht, solange, bis das als Wert der Eigenschaft e.target hinterlegte Zielelement erreicht ist.

Ist das Event also bei…

| h1 |

…angekommen, dann ist die Capturing-Phase abgeschlossen und das Event tritt in die sogenannte TARGET-PHASE ein.

Bei den meisten (aber nicht bei allen) Event-Typen passiert an dieser Stelle nun folgendes:

Die Liste der Vorfahrenelemente, also der Eventpfad wird umgedreht, so dass nunmehr das Zielelement an erster, und das globale Window-Objekt an letzter Stelle steht. Für unser Beispiel also:

| h1 | header | body | document | window |

Und jetzt beginnt das ganze Spiel von vorne, sprich, das Event durchläuft nun ausgehend vom Zielelement die ganze Kette der Vorfahrenelemente zurück bis hoch zu Window, und diese dritte Phase nennt man BUBBLING-PHASE.

Wenn man nun diese (in der Regel) drei Phasen zusammennimmt, sprich man auch vom Eventfluss (event-flow).

Die Phase, in welcher sich das Event gerade befindet, ist in der Event-Eigenschaft eventPhase hinterlegt, kann also wiefolgt ausgelesen werden:

element.addEventListener('click', function (e) {
  var phase = e.eventPhase  // z.B. 1 / 'CAPTURING_PHASE'
}, true);

Mit diesem Hintergrund wissen können wir uns also der Frage zuwenden, was Event Delegation bedeutet.

Event Delegation

Event Delegation bedeutet, ein Event auf seinem Weg durch das DOM an einer anderen Stelle abzufangen, als an dessen Zielelement.

Denn während das Event die Vorfahrenelemente des Event Target passiert, werden alle für diese Elemente registrierten Event Listener ausgelöst, die für diesen Event-Typ bestimmt sind!

Um also in unserem Beispiel zu bleiben, könnten wir den Event Listener statt für H1 zum Beispiel auch für BODY registrieren:

document.body.addEventListener('click', function (e) {
  if (e.target.tagName === 'h1') {
    // do something
  }
});

Das heißt es ist möglich, Ereignisse an einer anderen Stelle zu verarbeiten, als direkt am Zielelement.

Man kann also statt für jedes einzelne DOM-Objekt Event Listener zu registrieren, auch einen einzigen Eventlistener auf einer höheren Hierarchieebene registrieren, um die Selektion der Zielelemente dann mittels der in e.target hinterlegten Werte innerhalb der Callback-Funktion vorzunehmen.

Dadurch lässt sich der Code zur Eventverarbeitung deutlich übersichtlicher strukturieren.

Man könnte sich das Ganze also in etwa vorstellen wie die Fahrt mit einem Bus im öffentlichen Nahverkehr:

Das heißt, zunächst steht der Bus im Depot (Window), wo der Busfahrer einen Plan für seine Route überreicht bekommt (Eventpath), in dem alle seine Stationen vom Depot bis hin zur Endhaltestelle (Event Target) verzeichnet sind.

Dann fährt der Bus los, verlässt das Depot und steuert die erste Haltestelle an.

Steht da nun ein Passagier der…

  • auf einen Bus wartet (ist also ein Event Listener für dieses Objekt registriert)
  • und will der Passagier auch genau mit diesem Bus fahren (stimmt der Event-Typ)

…dann steigt der Passagier in den Bus ein (die Callback-Funktion des Event Listeners wird also aufgerufen).

Steht dort kein potentieller Fahrgast, fährt der Bus auf seiner festgelegten Route einfach weiter in Richtung seines Ziels, und bei jeder Haltestelle (jedem Element auf dem Eventpfad) wiederholt sich die Prozedur.

An seiner Endhaltestelle angekommen dreht der Bus dann um und fährt wieder zurück zum Depot, wobei dann (in der Regel) wieder alle Haltestellen abgeklappert werden.

Dabei ist aber nun folgendes zu beachten: Nicht jedes Event verfügt über eine Bubbling-Phase!

Bestimmte Events, wie z.B. focus oder load haben von Natur aus nur zwei Eventphasen, nämlich die Capturing-Phase und die Target-Phase.

Das bedeutet, dass wenn man solche Events delegieren will, man sie in ihrer Capturing-Phase abfangen muss, sprich, man muss das optionale dritte Argument für die addEventListener-Methode, deren Defaultwert false ist, auf true setzen:

document.addEventListener('focus', callback, true);

Hierbei ist zu beachten, dass, je nach dem welchen Wert die Capturing-Variable innehat und je nach dem ob das Event überhaupt bubbelt, die derart registrierten Event Listener nur entweder in ihrer Capturing-Phase oder in ihrer Bubbling-Phase ausgelöst werden.

Davon abgesehen ist aber noch interessant zu wissen, dass - um in unserem Beispiel zu bleiben - es nicht nur sein kann, dass der Bus von seiner Endhaltestelle (Event Target) direkt wieder zum Depot (Window) fährt, da es keine Bubbling-Phase gibt, sondern die Reise des Busses auch manuell unterbrochen werden kann, durch zwei Methoden der Event-Schnittstelle, nämlich stopPropagation( ) und stopImmediatePropagation( ):

document.body.addEventListener('click', function (e) {
  e.stopPropagation( );  // oder auch e.stopImmediatePropagation( );
});

Es ist also in etwa so, als wenn ein Fahrgast in den Bus einsteigt, dem Fahrer eine Knarre an die Rübe hält und anordnet, keine weiteren Haltestellen anzufahren (stopPropagation), sprich, keine Event Listener mehr auszulösen, die für andere Elemente registriert sind, als für dasjenige, welches als e.currentTarget hinterlegt ist.

Oder im Falle von stopImmediatePropagation( )` auch an dieser Haltestelle keine weiteren Fahrgäste mehr einsteigen zu lassen, also überhaupt keine anderen Event Listener mehr auszulösen, nicht nur bei anderen Elementen nicht, sondern auch keinen weiteren, für diesen Event-Typ in der mit dem Current Target assoziierten Liste von Event Listenern hinterlegten Handler mehr auszulösen.

Über das Geschriebene hinaus stellt die Schnittstelle Event noch weitere Eigenschaften und Methoden bereit, die ich aus Platzgründen hier jedoch nicht beschreibe, zumal das alles ja auch anderswo nachgelesen werden kann.

Zu erwähnen sei hier höchstens noch die Methode e.preventDefault, mit der sich, so dies grundsätzlich im Einzelfall möglich ist, die Ausführung einer Defaultaktion des User Agents aussetzen lässt. Ob dies möglich ist, kann über die Eigenschaft e.cancelable in Erfahrung gebracht werden.

Also das war jetzt eine Menge Text, aber wirklich schwer zu verstehen ist es eigentlich nicht.

Hoffe ich. ;-)

Gruß,

Orlok