JavaScript/DOM/Event/Ereignisbehandlung

Aus SELFHTML-Wiki
< JavaScript‎ | DOM‎ | Event(Weitergeleitet von Bubbling)
Wechseln zu: Navigation, Suche

Ereignisbehandlung (Event Handling)

Die Überwachung und Verarbeitung von Ereignissen (Events) ist einer der grundlegenden Aspekte der Programmierung mit JavaScript. Denn üblicherweise werden entsprechende Programme nicht linear durchlaufen, sondern die einzelnen Programmteile werden erst beim Eintritt bestimmter Ereignisse ausgeführt, wobei die Ereignisse, welche zur Steuerung des Kontrollflusses verwendet werden können, vielfältig sind. Ereignisse können durch Benutzereingaben wie beispielsweise Mausklicks ausgelöst werden oder auch Statusmeldungen des Browsers repräsentieren, wie etwa in dem Fall, dass eine Datei fertig geladen wurde oder wenn ein Fehler aufgetreten ist.

Die Ereignisbehandlung in JavaScript folgt dabei dem Prinzip der Steuerungsumkehrung (Inversion of Control), das bedeutet, innerhalb des Programms muss nicht etwa unter Verwendung der Methode setInterval oder durch den rekursiven Aufruf von setTimeout ständig nachgefragt werden, ob ein bestimmtes Ereignis eingetreten ist (Polling), sondern es genügt, an entsprechender Stelle eine Rückruffunktion (Callback Function) zu hinterlegen, welche dann beim Ereigniseintritt automatisch aufgerufen wird, sodass die darin enthaltenen Anweisungen in Folge des Ereignisses ausgeführt werden.

Grundsätzlich besteht die Behandlung von Ereignissen aus drei Komponenten, nämlich dem Objekt, auf welchem das Ereignis überwacht werden soll, dem Typ des Ereignisses sowie der Rückruffunktion, die aufgerufen werden soll wenn das Ereignis eingetreten ist. Man spricht hier von der Registrierung eines Event-Handlers für ein bestimmtes Objekt und einen bestimmten Ereignistyp, wobei man den Begriff Event-Handler etwa als Ereignisbehandler oder Ereignisverarbeiter übersetzen kann.

Aufgrund der historischen Entwicklung stehen heute mehrere Möglichkeiten zur Verfügung Event-Handler zu registrieren, wobei allerdings dringend angeraten sei, die Implementierung von Routinen zur Ereignisverarbeitung grundsätzlich unter Verwendung der modernen Methode addEventListener vorzunehmen, welche gegenüber den anderen etablierten Schnittstellen deutlich weniger fehleranfällig ist und auch darüber hinaus Vorteile besitzt.

Moderne Ereignisbehandlung

Wenn von moderner Ereignisbehandlung gesprochen wird, so ist damit die Überwachung und Verarbeitung von Ereignissen nach dem in der Spezifikation des Document Objekt Models definierten Ereignismodell gemeint. Nach diesem Modell erfolgt die Registrierung von Event-Handlern mittels der Methode addEventListener, deren Bezeichner soviel bedeutet wie füge einen Event-Listener hinzu. Der Begriff Event-Listener ist hierbei weitestgehend gleichbedeutend mit dem Begriff Event-Handler, wird jedoch in der Regel nur im Zusammenhang mit der genannten Methode verwendet und nicht bezogen auf Event-Handler, welche über andere Schnittstellen registriert werden.

Einrichtung der Ereignisüberwachung (addEventListener)

Die in praktisch allen heutzutage verwendeten Browsern implementierte Methode addEventListener ist Teil der Schnittstelle EventTarget, welche an alle Elemente sowie an einige andere durch die Ausführungsumgebung bereitgestellte Objekte vererbt wird, wie beispielsweise das globale Objekt window oder das Objekt document. Bevor wir uns nun jedoch mit der Funktionsweise der Methode addEventListener näher auseinandersetzen, sehen wir uns im Folgenden zunächst ein Beispiel für ihre praktische Anwendung an.

Es handelt sich dabei um einen so genannten Ready-Handler. Er ist erforderlich, wenn ein Script mit dem DOM interagieren soll, aber ausgeführt wird, bevor die verwendeten Objekte des DOM bereit ist.

Moderne Ereignisbehandlung
var handler = function ( ) {
  console.info('DOM is ready');
};

document.addEventListener('DOMContentLoaded', handler);

window.addEventListener('load', function ( ) {
  console.info('Document has finished loading');
});

In diesem Beispiel wird zunächst eine Variable mit dem Bezeichner handler deklariert und zugleich auch definiert, indem ihr als Wert eine anonyme Funktion zugewiesen wird, welche bei ihrem Aufruf eine Meldung in die Konsole schreibt. In einem zweiten Schritt wird die zuvor definierte Funktion als Event-Handler für das Objekt document und den Ereignistyp DOMContentLoaded registriert, indem auf dem Dokumentobjekt die Methode addEventListener aufgerufen wird. Der Methode werden bei ihrem Aufruf zwei Argumente übergeben, nämlich als erstes ein String mit dem Bezeichner DOMContentLoaded, welcher den Ereignistyp bestimmt für welchen der Event-Handler registriert werden soll, und als zweites eine Referenz auf die zuvor definierte Funktion mit dem Bezeichner handler.

Das Ereignis DOMContentLoaded tritt ein, sobald das Dokument fertig geladen und geparst wurde, das heißt, wenn die interne Objektrepräsentation der Webseite vollständig ist, und es kann ausschließlich unter Verwendung der Methode addEventListener überwacht werden. Beim Ereigniseintritt wird nun durch den Browser automatisch die Rückruffunktion handler aufgerufen und die darin enthaltene Anweisung ausgeführt.

Schließlich wird in dem Beispiel unter Verwendung der Methode addEventListener noch ein weiterer Event-Handler registriert, diesmal für das Fensterobjekt window und den Ereignistyp load. Im Gegensatz zum ersten Methodenaufruf wird hier nicht auf eine zuvor definierte Funktion referenziert, sondern es wird der Methode bei ihrem Aufruf direkt ein Funktionsobjekt als Argument übergeben, was immer dann die bessere Vorgehensweise darstellt, wenn ohnehin nur eine einzige Funktion als Event-Handler für ein bestimmtes Objekt und einen bestimmten Ereignistyp registriert werden soll, da man sich hier eine zusätzliche Variablendeklaration spart. Auch die auf diese Weise notierte Rückruffunktion enthält in ihrem Funktionskörper die Anweisung eine Meldung in die Konsole zu schreiben.

Anders als das Ereignis DOMContentLoaded, welches bereits eintritt wenn das Dokument selbst fertig geladen und geparst wurde, wird das Ereignis load auf den Objekten document oder window erst dann ausgelöst, wenn darüber hinaus auch alle in das Dokument eingebundenen Ressourcen wie zum Beispiel Stylesheets oder Bilder fertig geladen wurden, was prinzipiell mehr Zeit in Anspruch nimmt und die Ausführung der mit dem Ereignis verknüpften Anweisungen verzögert.

Im Ergebnis sind also beide Ereignistypen grundsätzlich geeignet als Einstiegspunkt (Entry Point) für die Programmausführung zu dienen. Für diesen Zweck wird in aller Regel jedoch das Ereignis DOMContentLoaded die bessere Wahl darstellen, zumal es auch möglich ist, mittels des Ereignisses load auf den Abschluss des Ladevorgangs bei einzelnen Ressourcen wie etwa Bildern zu reagieren, sodass durch die Kombination der beiden Ereignistypen ein an den jeweiligen Bedarf angepasster Programmeinstieg möglich ist.

Programmeinstieg
document.addEventListener('DOMContentLoaded', function ( ) {
  console.info('DOM is ready');
  document.images[0].addEventListener('load', function ( ) {
    console.info('Image is ready');
  });
});

In diesem Beispiel wird zunächst ein Event-Handler für das Ereignis DOMContentLoaded registriert. Sobald nun die interne Repräsentation der Dokumentstruktur vollständig ist, wird das Ereignis ausgelöst und eine Bestätigung in die Konsole geschrieben. Darüber hinaus wird innerhalb der Rückruffunktion ein weiterer Event-Handler registriert, welcher aufgerufen wird, sobald ein bestimmtes in die Webseite eingebundenes Bild fertig geladen wurde. Dabei wird das Objekt, welches das img-Element repräsentiert, über die Objekteigenschaft images des Dokumentobjektes referenziert, welche eine HTML-Collection der im Dokument vorkommenden Elemente dieses Typs enthält, auf die wie in einem Array über einen Index zugegriffen werden kann. Da hier im Beispiel der Zugriff über den Schlüssel 0 erfolgt, wird das Objekt referenziert, welches das erste im Dokument vorkommende img-Element repräsentiert und der Event-Listener entsprechend für dieses Objekt registriert.

Auf diese Weise wird nun die Programmausführung nicht solange verzögert, bis wirklich alle eingebundenen Ressourcen geladen sind, sondern es können diejenigen Anweisungen für welche der Status des Bildes irrelevant ist, bereits ausgeführt werden bevor dieses fertig geladen wurde.

Beachten Sie: Die gezeigte Vorgehensweise ist nur dann zielführend, wenn Sie genau wissen, dass Ihr Script ausgeführt wird, während die Eigenschaft readyState des Dokument-Objekts noch auf 'loading' steht. Bei asynchron ausgeführten Scripten, oder solchen, die nachträglich geladen werden, ist das nicht der Fall. Wenn der readyState nicht mehr auf 'loading' steht, dürfen Sie keinen Handler für das DOMContentLoaded Event mehr registrieren, er wird nicht mehr aufgerufen.

Nachdem wir nun also anhand einiger Anwendungsbeispiele gesehen haben, wie mittels der Methode addEventListener Ereignisse behandelt werden können, wird es Zeit, die Funktionsweise der Methode einmal etwas genauer zu betrachten, wobei zunächst einmal auf einen wesentlichen Unterschied und Vorteil im Vergleich zu den anderen etablierten Schnittstellen einzugehen ist. Während die Registrierung von Event-Handlern bei der traditionellen Ereignisbehandlung nämlich über Eigenschaftszuweisung erfolgt, was immer das Risiko des unbeabsichtigten Überschreibens birgt, werden durch die Methode addEventListener hinzugefügte Event-Handler in einer internen, mit dem Objekt assoziierten Liste hinterlegt. Durch diese zusätzliche Abstraktionsschicht wird demnach einerseits eine häufige Fehlerquelle beseitigt, sodass entsprechende Implementierungen im Vergleich deutlich robuster sind, und andererseits wird es dadurch überhaupt erst möglich, mehr als einen Event-Handler für das selbe Objekt und den selben Ereignistyp zu registrieren.


Mehrere Event-Handler
var button = document.getElementById('button');

button.addEventListener('click', function ( ) {
  console.log('Hello');
});

button.addEventListener('click', function ( ) {
  console.log('World');
});


In diesem Beispiel wird zunächst unter Verwendung der Methode getElementById des Dokumentobjektes ein Element mit einer bestimmten ID referenziert und einer gleichnamigen Variable als Wert zugewiesen, wobei wir uns hier einmal vorstellen, es würde sich bei dem referenzierten Objekt um ein zuvor im HTML-Code notiertes button-Element handeln. Für dieses Objekt und für das Ereignis click werden nun zwei Event-Handler registriert, wobei beide Male eine anonyme Funktion als Argument übergeben wird. Wenn nun auf den Button geklickt wird, so wird infolgedessen nacheinander Hello und World in die Konsole geschrieben, denn die Event-Handler werden beim Ereigniseintritt grundsätzlich in der Reihenfolge aufgerufen, in welcher sie registriert wurden, zumindest sofern die Registrierung für dieselbe Ereignisphase erfolgt ist. Aber dazu später mehr.

Jedenfalls ist in diesem Zusammenhang zu beachten, dass zwar mehrere Event-Listener für das selbe Objekt und den selben Ereignistyp registriert werden können, jedoch nicht dieselbe Registrierung mehrfach vorgenommen werden kann. Beim Aufruf der Methode addEventListener wird nämlich intern zunächst geprüft, ob in der mit dem jeweiligen Objekt verbundenen Liste der Event-Listener bereits ein Eintrag exisitert, welcher den bei ihrem Aufruf übergebenen Argumenten entspricht. Wird die Methode also auf dem gleichen Objekt mehr als einmal mit denselben Argumenten aufgerufen, so wird nur beim ersten Aufruf ein entsprechender Event-Listener registriert, während alle weiteren Aufrufe folgenlos bleiben.


Keine doppelte Registrierung
var handler = function ( ) {
  console.info('hash changed: ' + location.hash);
};

window.addEventListener('hashchange', handler);
window.addEventListener('hashchange', handler);


Hier ist zunächst eine Funktion namens handler notiert, welche die Anweisung beinhaltet eine Meldung in die Konsole zu schreiben, die den in der Eigenschaft hash des von der Browserschnittstelle zur Verfügung gestellten Objektes location hinterlegten Anker der Seite beinhaltet. Weiterhin wird auf dem Fensterobjekt window zweimal die Methode addEventListener aufgerufen, wobei beide Male der Ereignistyp hashchange und die Rückruffunktion handler als Argumente übergeben werden. Das Ereignis hashchange wird ausgelöst, sobald durch den Klick auf einen internen Link zu dessen Verweisziel gesprungen wird und sich dementsprechend der in der Eigenschaft location.hash gespeicherte Anker ändert.

Dieser doppelte Methodenaufruf führt nun zwar nicht dazu, dass ein Fehler geworfen wird, jedoch bleibt der zweite Aufruf folgenlos, da bei diesem Aufruf dieselben Argumente übergeben wurden wie beim ersten, sodass der Event-Handler hier nur einmal registriert wird und die Rückruffunktion beim Eintritt des Ereignisses dem zur Folge auch nur einmal aufgerufen wird.

Schließlich ist im Zusammenhang mit der Methode addEventListener noch zu beachten, dass auf diese Weise registrierte Event-Handler an das jeweilige Objekt gebunden sind, sie dem zur Folge also nicht mitkopiert werden, wenn das Objekt geklont wird.


Objektgebundenheit
var element = document.getElementById('name');

element.addEventListener('click', function ( ) {
  console.log('element clicked');
});

var clone = element.cloneNode(true);
document.body.appendChild(clone);


In diesem Beispiel wird zunächst ein bestimmtes Element mittels der Methode getElementById des Dokumentobjektes referenziert und der Variable element als Wert zugewiesen, bevor für dieses Element und das Ereignis click ein Event-Listener registriert wird. In einem nächsten Schritt wird dann unter Verwendung der von der Schnittstelle Node des Document Object Models vererbten Methode cloneNode eine Kopie dieses Elementes erstellt, welche dann durch Aufruf der ebenfalls von der Schnittstelle Node geerbten Methode appendChild dem Element body als Kindelement zugewiesen wird. Wird nun auf dieses geklonte Element geklickt, passiert nichts, da der für dessen Vorlage registrierte Event-Listener beim Klonen nicht mitkopiert wurde.

Beendigung der Ereignisüberwachung (removeEventListener)

Eine mittels der Methode addEventListener implementierte Ereignisüberwachung kann auch wieder beendet werden, wobei die Entfernung eines zuvor auf diese Weise registrierten Event-Handlers durch den Aufruf der Schwestermethode removeEventListener erfolgt. Dabei sind der Methode removeEventListener exakt dieselben Argumente zu übergeben, welche zuvor bei der Registrierung des Event-Handlers übergeben wurden.


Beendigung der Ereignisüberwachung
var tick = function ( ) {
  console.log('All work and no play makes Jack a dull boy');
};

window.addEventListener('mousemove', tick);

document.body.addEventListener('mousedown', function ( ) {
  console.warn("Don't repeat yourself!");
  window.removeEventListener('mousemove', tick);
});


Hier wird zunächst auf dem Objekt window eine Überwachung des Ereignisses mousemove eingerichtet bei der eine Referenz auf die Funktion tick übergeben wird, sodass immer wenn die Maus sich innerhalb des Browserfensters bewegt, eine Meldung in die Konsole geschrieben wird. In einem zweiten Schritt wird dann für das Element body, welches hier über die gleichnamige Eigenschaft des Dokumentobjektes angesprochen wird, ein Event-Handler für das Ereignis mousedown registriert, welches ausgelöst wird, wenn eine Maustaste gedrückt wird. In der anonymen Rückruffunktion ist nun einerseits die Anweisung enthalten, eine Warnung in die Konsole zu schreiben, sowie andererseits ein Aufruf der Methode removeEventListener auf dem Fensterobjekt, welche die zuvor implementierte Ereignisüberwachung für das Ereignis mousemove und die Handlerfunktion tick wieder beendet.

Hinsichtlich der Verwendung der Methode removeEventListener ist allerdings zu beachten, dass durch sie nur solche Ereignisüberwachungen wieder beendet werden können, bei deren Einrichtung mittels der Methode addEventListener auf eine externe Funktion referenziert wurde. Denn direkt bei der Argumentübergabe notierte Funktionen sind immer eigenständige Funktionsobjekte, unabhängig davon, ob es sich um anonyme Funktionen handelt oder nicht, sodass hierbei die erforderliche Identität der Argumente prinzipiell ausgeschlossen ist.


Unterschiedliche Funktionsobjekte
document.body.addEventListener('click', function logTrue ( ) {
  console.log(true);
});

document.body.removeEventListener('click', function logTrue ( ) {
  console.log(true);
});


In diesem Beispiel wird zunächst für das Element body und das Ereignis click ein Event-Listener registriert, wobei die Rückruffunktion direkt bei der Argumentübergabe definiert wird. Bei dem nachfolgenden Aufruf der Methode removeEventListener sieht es nun so aus, als würden exakt dieselben Argumente übergeben, aber dies ist nicht der Fall, denn obschon beide Funktionen hier sogar denselben Bezeichner haben, handelt es sich hier doch um unterschiedliche Funktionsobjekte, weshalb keine Identität der Argumente vorliegt und entsprechend die zuvor eingerichtete Ereignisüberwachung durch den Aufruf der Methode removeEventListener nicht beendet wird.

Das Ereignisobjekt

Ereignisse sind in JavaScript selbst Objekte, welche über eine Vielzahl an Eigenschaften und Methoden verfügen. Die grundlegende Funktionalität wird dabei von der Schnittstelle Event des Document Object Models an die einzelnen Ereignisobjekte vererbt. Wird eine Handlerfunktion beim Eintritt eines Ereignisses aufgerufen, so wird ihr immer das Objekt, welches das auslösende Ereignis repräsentiert, als Argument übergeben, sodass innerhalb der Funktion damit gearbeitet werden kann.


Zugriff auf das Ereignisobjekt
document.documentElement.addEventListener('mouseup', function (event) {
  console.log(event.type + ' on ' + event.currentTarget.nodeName);
});


In diesem Beispiel wird für das Element html, welches über die Eigenschaft documentElement des Dokumentobjektes referenziert wird, ein Event-Handler für das Ereignis mouseup registriert, welches ausgelöst wird, wenn eine Maustaste losgelassen wird. Für die der Methode addEventListener als zweites Argument übergebene anonyme Funktion wird ein Parameter mit dem Bezeichner event deklariert, welcher dann beim Funktionsaufruf automatisch mit dem Ereignisobjekt initialisiert wird.

Im Funktionskörper der Rückruffunktion ist nun die Anweisung notiert, eine Meldung in die Konsole zu schreiben, welche zunächst den Rückgabewert der Eigenschaft type des Ereignisobjektes beinhaltet. Der Wert dieser Eigenschaft ist ein String, welcher den Bezeichner des Ereignistyps enthält, hier also mouseup, und der demzurfolge identisch ist mit dem der Methode addEventListener als erstem übergebenen Argument.

Weiterhin wird hier auf die Eigenschaft currentTarget des Ereignisobjektes zugegriffen, welche das Objekt zurückgibt, für das der aufgerufene Event-Handler registriert wurde, hier also das Objekt, welches das Element html repräsentiert. Entsprechend ist der Wert dieser Eigenschaft identisch mit dem Wert, mit welchem die Funktionsvariable this beim Aufruf einer Funktion als Event-Handler initialisiert wird, denn auch this verweist in diesem Fall immer auf das Objekt, für welches der aufgerufene Event-Handler registriert wurde.

Auf diesem Objekt wird schließlich die von der Schnittstelle Node des Document Object Models vererbte Eigenschaft nodeName ausgelesen, welche den Namen des Knotens zurückgibt, sodass beim Eintritt des Ereignisses infolgedessen mouseup on HTML in die Konsole geschrieben wird.


Zeitstempel und alternative Notation
document.body.firstElementChild.addEventListener('dblclick', function (e) {
  console.log('Time: ' + e.timeStamp);
});


In diesem weiteren Beispiel zur Arbeit mit dem Ereignisobjekt wird für das erste Kindelement von body und das Ereignis dblclick ein Event-Handler registriert. Das Ereignis dblclick wird bei einem Doppelklick ausgelöst (double click). Als Parameter der Rückruffunktion wird der Bezeichner e deklariert, was die gängige Notation für mit Ereignissen initialisierte Parameter darstellt. Wird nun doppelt auf das Element geklickt, so wird der Wert der Objekteigenschaft timeStamp in die Konsole geschrieben, der millisekundengenau den Zeitpunkt markiert, an dem das Ereignis eingetreten ist.

Standardaktionen aussetzen (preventDefault)

Einige durch die Ausführungsumgebung ausgelöste Ereignisse sind mit einer Standardaktion (Default Action) verbunden, wie zum Beispiel in dem Fall, dass auf einen Verweis, also ein a-Element geklickt wird und infolgedessen als Standardaktion durch den Browser das in dem Attribut href des Elementes hinterlegte Verweisziel angesteuert wird. Hin und wieder ist es nun gewünscht, dass eine solche Standardaktion nicht ausgeführt wird, und es besteht demzufolge die Notwendigkeit, dem Browser diesen Wunsch mitzuteilen. Dies kann durch den Aufruf der Methode preventDefault des Ereignisobjektes bewerkstelligt werden.


Verhinderung einer Standardaktion
var links = document.links, len = links.length, i;
for (i = 0; i < len; i += 1) {
  links[i].addEventListener('click', function (e) {
    if (e.cancelable) {
      e.preventDefault( );
      console.info(e.defaultPrevented);
    }
  });
}


Hier werden zunächst einmal einige Variablen deklariert. Der Variable mit dem Bezeichner links wird der Wert der gleichnamigen Eigenschaft des Objektes document zugewiesen, bei dem es sich um eine HTML-Collection der im Dokument vorkommenden Elemente a und area handelt, deren href-Attribut gesetzt ist, auch wenn als Attributwert nur ein Leerstring notiert wurde. Der Variable mit dem Bezeichner len wird der Wert der Objekteigenschaft length der Collection zugewiesen, welche die Anzahl der enthaltenen Elemente zurückgibt. Schließlich wird noch eine Zählvariable i für die nachfolgende Schleife deklariert.

In dieser for-Schleife wird nun über die entsprechenden Elemente iteriert und für jedes dieser Elemente ein Event-Handler registriert, wobei für die Rückruffunktion ein Parameter e deklariert wird, welcher dann beim Eintritt des Ereignisses, wenn die Handlerfunktion aufgerufen wird mit dem Ereignisobjekt initialisiert wird.

Im Funktionskörper der Handlerfunktion wird nun mittels einer bedingten Anweisung zunächst geprüft, ob der Wert der Eigenschaft cancelable des Ereignisobjektes true ist, was hier allerdings nur zu Demonstrationszwecken erfolgt, da das Ergebnis der Überprüfung in diesem Fall immer positiv ausfällt und man sich die Abfrage dementsprechend eigentlich sparen kann. Jedenfalls ist eine solche Abfrage geeignet um herauszufinden, ob bei einem Ereignis, welches mit einer Standardaktion verbunden ist, diese durch Aufruf der Methode preventDefault ausgesetzt werden kann.

Dieser Methodenaufruf ist nun als nächstes notiert, sodass infolgedessen beim Klick auf einen Verweis innerhalb des Dokumentes die Standardaktion zum Verweisziel zu springen ausgesetzt wird. Eine Bestätigung, dass diese Aktion auch tatsächlich verhindert wurde, wird aufgrund der zuletzt eingefügten Anweisung in die Konsole geschrieben, bei welcher der Wert der Eigenschaft defaultPrevented ausgelesen wird, die entsprechend true oder false zurückgibt, je nachdem ob die mit dem Ereignis verknüpfte Standardaktion ausgesetzt wurde oder nicht.

Der Ereignisfluss (Event Flow)

Bislang kennengelernt haben wir das Schema, nach dem ein Ereignis bei einem bestimmten Objekt eintritt, wo es durch einen für dieses Objekt registrierten Event-Handler abgefangen und verarbeitet werden kann. Tatsächlich ist das als Teil des Document Object Models spezifizierte Ereignismodell jedoch sehr viel komplexer aufgebaut, denn Ereignisse sind nicht lediglich mit dem Objekt verknüpft, bei dem sie aufgetreten sind, sondern sie bewegen sich gewissermaßen durch die Objektstruktur hindurch, welche die Webseite intern repräsentiert. Das Modell, welches diese Bewegung der Ereignisse beschreibt, bei der man auch von Verbreitung (Propagation) spricht, wird Ereignisfluss (Event Flow) genannt.

Bevor wir nun jedoch tiefer in die Materie einsteigen, sei zunächst anhand eines kleinen Beispiels illustriert, weshalb eine statische Verknüpfung von einem Objekt und einem bei diesem Objekt eingetretenen Ereignis alles andere als vorteilhaft wäre, und weshalb die Verbreitung von Ereignissen über die entsprechenden Objekte hinaus eine notwendige Voraussetzung für eine effiziente Ereignisbehandlung darstellt.


Verschachtelte Elemente
<p>Lorem ipsum dolor sit amet, <em>consectetuer adipiscing</em> elit.</p>


In diesem Beispiel ist zunächst ein Absatz (paragraph) notiert, also ein p-Element, von dessen textuellem Inhalt ein Abschnitt mittels eines em-Elementes als hervorgehoben (emphasized) ausgezeichnet ist. Wenn wir nun für das Objekt, welches das Element p repräsentiert, einen Event-Handler registrieren würden, beispielsweise für das Ereignis click, so würden wir zurecht erwarten, dass dieser Handler bei jedem Klick auf den zwischen dem öffnenden und schließenden Tag des Absatzes notierten Text aufgerufen wird, da dieser schließlich den Inhalt des Elementes darstellt.

Nun würde aber in dem Fall, dass Ereignisse tatsächlich nur den Aufruf solcher Event-Handler verursachen würden, welche für das Objekt registriert wurden bei dem das jeweilige Ereignis aufgetreten ist, hier der für das Element p registrierte Event-Handler in Folge eines Klicks auf den hervorgehobenen Text nicht aufgerufen werden, da dieser Text Inhalt eines anderen Objektes ist, nämlich desjenigen, welches das Element em repräsentiert.

Eine statische Bindung von Ereignissen an die Objekte, bei denen sie passieren, würde demnach dazu führen, dass in dem häufig auftretenden Fall, dass ein Ereignis nicht nur bei einem Element, sondern auch bei dessen Kindelementen überwacht werden soll, für jedes dieser Elemente ein eigener Event-Handler registriert werden müsste, was selbstverständlich nicht sonderlich effizient wäre. Aus diesem Grund propagieren Ereignisse also durch die interne Objektstruktur des Dokumentes, und nach welchen Regeln sie dies tun, sehen wir uns im Folgenden genauer an.

An erster Stelle steht hierbei nun tatsächlich die Verknüpfung zwischen dem Ereignis und dem Objekt bei dem es eingetreten ist, denn wenn bei einem bestimmten Objekt ein Ereignis passiert, dann wird dieses Objekt zunächst intern als Zielobjekt (Target) markiert, und eine Referenz auf dieses Zielobjekt wird dem von einem Konstruktor erzeugten Ereignisobjekt als Wert der Eigenschaft target zugewiesen, auf die natürlich auch innerhalb einer Handlerfunktion entsprechend zugegriffen werden kann.


Zugriff auf das Zielobjekt
document.body.lastElementChild.addEventListener('click', function (e) {
  console.info('Click on ' + e.target.tagName);
});


In diesem Beispiel ist wird für das letzte Kindelement von body und das Ereignis click ein Event-Handler registriert, für den ein Parameter e deklariert wird, welcher dann beim Funktionsaufruf mit dem Ereignisobjekt initialisiert wird. Innerhalb der Rückruffunktion ist die Anweisung notiert, eine Meldung in die Konsole zu schreiben, wobei auf die Eigenschaft target des Ereignisobjektes zugegriffen wird, welche wie gesehen eine Referenz auf das Objekt enthält, bei dem das Ereignis eingetreten ist.

Von diesem Objekt wird der Rückgabewert der von der Schnittstelle Element des Document Object Models an alle Elemente vererbten Eigenschaft tagName ausgelesen, bei dem es sich um den Namen des jeweiligen Elementes handelt.

Würde nun auf das letzte Kindelement von body geklickt, so wäre das Objekt, welches dieses Element repräsentiert, das Zielobjekt, das in der Objekteigenschaft target hinterlegt ist, und da hier der Event-Handler obendrein auch genau für dieses Element registriert wurde, wäre nebenbei bemerkt die Referenz auf dieses Element ebenfalls der Wert der Eigenschaft currentTarget des Ereignisobjektes, sowie der Wert der Funktionsvariable this.

Das Zielobjekt ist dem zur Folge also gewissermaßen der Ausgangspunkt beim Versand von Ereignissen (Event Dispatch) innerhalb der internen Objektstruktur einer Webseite. Allerdings ist es nicht das erste Objekt, welches von dem durch die Struktur propagierenden Ereignisobjekt erreicht wird, wie wir gleich noch sehen werden.

Der Ereignispfad (Event Path)

Nachdem also das Zielobjekt festgelegt wurde, bei dem das Ereignis aufgetreten ist, wird in einem nächsten Schritt intern eine Liste der Vorfahren des Zielobjektes angelegt. Diese Liste enthält neben dem Zielobjekt selbst auch alle Objekte, die innerhalb der Baumstruktur der Repräsentation des Document Object Models zwischen dem globalen Objekt window und dem Zielobjekt liegen, und zwar zunächst in Richtung des Baums, also beginnend mit der Wurzel, dem Fensterobjekt window.

Diese Liste der Vorfahren des Zielobjektes beschreibt den Pfad, auf welchem das Ereignisobjekt die Objektstruktur durchqueren soll, weshalb hierbei von einem Ereignispfad (Event Path) beziehungsweise Verbreitungspfad (Propagation Path) gesprochen wird.

Um einmal zu veranschaulichen wie so ein Ereignispfad aussehen könnte, sehen wir uns das folgende Beispiel eines HTML-Dokumentes an, in dem eine Ereignisüberwachung für ein bestimmtes Element implementiert ist.


Fragment eines HTML-Dokumentes
<!doctype html>

<html lang="en">
  <!-- document head -->
  <body>
    <header>
      <h1 id="heading">Hello World</h1>
    </header>
    <!-- main content -->
    <script>
      var heading = document.getElementById('heading');

      heading.addEventListener('click', function ( ) {
        console.log(this.textContent);
      });
    </script>
  </body>
</html>


Hier ist innerhalb von body als erstes ein header-Element notiert, welches wiederum eine Überschrift h1 als Kindelement hat. Das Objekt, welches dieses Element h1 repräsentiert, wird nun über die ID heading mittels der Dokumentmethode getElementById referenziert und einer gleichnamigen Variable als Wert zugewiesen. Für dieses Objekt und den Ereignistyp click wird sodann ein Event-Handler registriert, der die Anweisung beinhaltet, eine Meldung in die Konsole zu schreiben. Dabei wird das Objekt, welches das h1-Element repräsentiert, über die Funktionsvariable this referenziert und es wird der Wert der Eigenschaft textContent ausgelesen, welche von der Schnittstelle Node des Document Object Models vererbt wird und die den textuellen Inhalt eines Knotens zurückgibt, in diesem Fall also den String Hello World.

Wenn nun also diese Webseite im Browser dargestellt und mit der Maus auf die Überschrift geklickt wird, dann wird intern der folgende Ereignispfad festgelegt, der den Weg des Ereignisobjektes durch das DOM vorzeichnet: Der erste Listeneintrag ist obligatorisch das globale Objekt window und an zweiter Stelle steht das Objekt document, welches das angezeigte Dokument repräsentiert. Der dritte Eintrag ist das Element html und der vierte dessen Kindelement body. Danach folgt an fünfter Stelle das Objekt, welches das header-Element repräsentiert und welches das Elternelement der Überschrift h1 und das Kindelement von body ist. Der sechste und letzte Listeneintrag, der den Ereignispfad schließlich komplettiert, ist dann die Überschrift h1 selbst, die hier das Zielobjekt darstellt.

Die drei Ereignisphasen

Auf der Grundlage des erzeugten Ereignispfades wird das Ereignisobjekt dann in der Folge in die Objektstruktur des DOM eingespeist. Dabei unterteilt sich dessen Verbreitung innerhalb der Struktur in der Regel in drei, mindestens jedoch in zwei Phasen (Event Phases), nämlich die Capturing-Phase, die Target-Phase und bei den meisten Ereignistypen zusätzlich die Bubbling-Phase, und zwar in dieser Reihenfolge.

Capturing

Wie wir im vorangegangenen Abschnitt gesehen haben, enthält die interne Liste, die den Ereignispfad repräsentiert, die Objekte, welche die Vorfahren des Zielobjektes innerhalb der Baumstruktur darstellen, beginnend mit dem globalen Objekt window und endend mit dem Zielobjekt selbst. Beim sogenannten Capturing, was etwa als einfangen übersetzt werden kann, wird das Ereignisobjekt nun an die in der Liste enthaltenen Objekte übergeben, die es dann in deren Reihenfolge passiert, wobei alle für diese Ereignisphase auf den jeweiligen Objekten registrierten Event-Listerner aufgerufen werden.

Dabei ist zu beachten, dass Ereignisse während dieser Phase ausschließlich mittels der Methode addEventListener überwacht und verarbeitet werden können, welche für diesen Zweck einen optionalen dritten Parameter besitzt.

Soll ein Ereignis also bereits in der Capturing-Phase abgefangen werden, so ist der Methode addEventListener bei ihrem Aufruf als drittes Argument der Wert true zu übergeben. Wird wie in allen bisherigen Beispielen hingegen kein drittes Argument übergeben, so wird der Event-Listener als Standardeinstellung grundsätzlich nicht innerhalb dieser Ereignisphase aufgerufen. Das heißt, wenn das Ereignis nicht in der Capturing-Phase abgefangen werden soll, genügt es auf die Übergabe eines dritten Argumentes beim Methodenaufruf zu verzichten und es muss nicht etwa der Wert false übergeben werden.

Target-Phase

Wird nach der Passage seiner Vorfahren also schließlich das Zielobjekt erreicht, sprich das Objekt, welches an letzter Stelle innerhalb des Ereignispfades steht, dann ist die Capturing-Phase beendet und das Ereignis tritt in die sogenannte Zielphase (Target Phase) ein. Hier werden nun alle Event-Handler aufgerufen, welche direkt für das Zielobjekt registriert wurden.

Bubbling

Anschließend wird, zumindest bei den allermeisten Ereignistypen, die Liste welche den Ereignispfad darstellt umgedreht, sodass nunmehr das Zielobjekt an erster Stelle und das Fensterobjekt window an letzter Stelle steht. Danach passiert das Ereignisobjekt die Objekte die den Ereignispfad konstituieren noch einmal, nur diesmal in umgekehrter Reihenfolge. Diesen Vorgang nennt man Bubbling, was sich sinngemäß in etwa als aufsteigen übersetzen lässt. Dabei werden alle Event-Handler aufgerufen, die für die entsprechenden Objekte innerhalb des Pfades und für diese Ereignisphase registriert wurden.

Dabei ist zu beachten, dass diejenigen Event-Handler, welche für die Capturing-Phase registriert wurden und die demnach zu diesem Zeitpunkt bereits aufgerufen wurden, in der Bubbling-Phase nicht noch einmal aufgerufen werden, sodass eine Registrierung immer nur für die eine oder die andere Ereignisphase erfolgen kann.

Wird bei der Registrierung eines Event-Listeners nicht explizit etwas anderes bestimmt, so bezieht sich die Registrierung also grundsätzlich nur auf die Target-Phase oder, sofern der Ereignistyp über eine solche verfügt, auf die Bubbling-Phase des Ereignisses. Dabei ist die Phase, in welcher sich das Ereignisobjekt an einem bestimmten Punkt gerade befindet, in der Eigenschaft eventPhase des Ereignisobjektes hinterlegt, deren Wert dem zur Folge innerhalb einer Handlerfunktion ausgelesen werden kann.


Die Objekteigenschaft eventPhase
document.body.addEventListener('mouseover', function (e) {
  console.log('current event phase: ' + e.eventPhase);
});


In diesem Beispiel wird für das Element body und den Ereignistyp mouseover ein Event-Handler registriert. Das Ereignis mouseover wird ausgelöst, wenn sich der Mauszeiger über ein Element bewegt. Die Rückruffunktion beinhaltet hier nun die Anweisung, eine Meldung in die Konsole zu schreiben, wobei der Wert der Eigenschaft eventPhase des Ereignisobjektes ausgelesen wird, mit dem der Parameter e beim Funktionsaufruf initialisiert wurde.

Die in der Eigenschaft eventPhase hinterlegten Werte sind in der Regel nicht die Bezeichner der Ereignisphasen selbst, sondern eine numerische Repräsentation, wobei der Wert 1 die Capturing-Phase repräsentiert, der Wert 2 die Zielphase und der Wert 3 die Bubbling-Phase. Je nach dem, ob hier im Beispiel nun der Mauszeiger über das Element body selbst oder über dessen Kindelemente bewegt wird, wird folglich also entweder der Wert 2 oder der Wert 3 in die Konsole geschrieben.

Beipielseite zu den Ereignisphasen

Mit dieser Testseite soll das Event-Capturing und -Bubbling veranschaulicht werden. Dazu wurden mehrere Divs mit farbigem Hintergrund ineinander verschachtelt.

Für jedes dieser Divs kann ein Eventhandler für pointerup, pointerdown, keyup und keydown registriert werden. Mit useCapture true werden die Eventhandler in der Capturing-Phase ausgeführt, mit useCapture false in der Bubbling-Phase.

Die Eventhandler schreiben einen Eintrag in die Browserkonsole und lassen für eine kurze Zeit eine Textinfo erscheinen. Die Info erscheint zeitverzögert, um die Abfolge auch verfolgen zu können.

Das hellblaue Div ist absolut positioniert und verschoben.

Bitte mit Maus, Finger oder Stift in eines der farbigen Rechtecke klicken, oder die Divs mit der Tabulatortaste anwählen und dann eine Taste drücken.

Eventphasen zeigen und Events mit stopPropagation an der Ausbreitung hindern ansehen …
// Eventhandler nach Wunsch registrieren
if(document.querySelector("#downFalse").checked) {
	eventElement.addEventListener("pointerdown", handleEventUseCaptureFalse, false);
	eventElement.addEventListener("keydown", handleEventUseCaptureFalse, false);
}
if(document.querySelector("#downTrue").checked) {
	eventElement.addEventListener("pointerdown", handleEventUseCaptureTrue, true);
	eventElement.addEventListener("keydown", handleEventUseCaptureTrue, true);
}
if(document.querySelector("#upFalse").checked) {
	eventElement.addEventListener("pointerup", handleEventUseCaptureFalse, false);
	eventElement.addEventListener("keyup", handleEventUseCaptureFalse, false);
}
if(document.querySelector("#upTrue").checked) {
	eventElement.addEventListener("pointerup", handleEventUseCaptureTrue, true);
	eventElement.addEventListener("keyup", handleEventUseCaptureTrue, true);
}

// Die Eventhandler
function handleEventUseCaptureTrue(event) {
	eventHandler(this, event, true);
}

function handleEventUseCaptureFalse(event) {
	eventHandler(this, event, false);
}

function eventHandler(eventElement,event,useCapture) {
	// Wenn Checkbox für Stoppropagation angehakt, Event nicht weiterreichen, kein bubbling und kein capturing
	if(chkStopPropagationElement.checked) event.stopPropagation();

	// Auf diese Tastaturereignisse soll nicht reagiert werden
	if(event.key == "Tab" || event.key == "Shift" || event.key == "Alt" || event.repeat) return;

	// Die drei Eventphasen
	let phase = event.eventPhase;
	if(event.eventPhase == 1) phase = "Capturing-Phase";
	else if(event.eventPhase == 2) phase = "Zielphase";
	else if(event.eventPhase == 3) phase = "Bubbling-Phase";

	// Konsolenausgabe
	console.log(event.type,eventElement.id,phase,"useCapture: "+useCapture);

	// Events sichtbar machen
	visualizeEvents.add(eventElement,event,phase,useCapture);
}

Auf dieser Testseite werden Divs als anklickbare Elemente verwendet. Dadurch ist die Seite nicht zugänglich. Das ist aber auf einer Testseite ok.