Als Web-Entwickler fügen wir im Laufe eines Projektes einer Website eine zumeist nicht unerhebliche Anzahl an JavaScript-Event-Handlern hinzu – sei es mit jQuery oder regulärem JavaScript (You Might Not Need jQuery). Abhängig von der gewählten Methode lässt sich damit… die Performance einer Website gründlich ruinieren.

Aber das muss nicht sein – wie dieser Überblick über die Montage von Event-Handlern in JavaScript / jQuery zeigt.

Ganz grundsätzlich muss jeder Prozessschritt beim Hinzufügen eines Event-Handlers richtig angewendet werden. Der ganze Vorgang besteht aus drei Schritten:

  1. Die DOM-Elemente selektieren,
  2. auf dem selektierten DOM-Elementen einen Event-Typ beobachten,
  3. und schließlich bei Auslösen des Events einen Event-Listener ausführen.

In jedem dieser Schritte lässt sich zum Teil massiv optimieren.

Kenne deine Selektoren

Um einen Event-Handler montieren zu können, muss dieser an ein Element angekoppelt werden – in der Regel ist dies ein DOM-Element. Dazu gibt es verschiedene Methoden, DOM-Elemente zu selektieren. Je nach gewählter Methode ist dies mehr oder weniger performant.

Faustformel: Je eindeutiger das Suchmerkmal und je kleiner die Menge der zu durchsuchenden Elemente, desto schneller ist die Suche.

Die schnellste Methode ist dabei die Selektion über ein id-Attribut – die langsamste dagegen die Suche nach einem beliebigen Attribut, im schlimmsten Fall mit der Prüfung, ob dieses Attribut einen bestimmten Wert beinhaltet.

DOM-Selektions-Methoden, sortiert nach Performance
Methode jQuery JavaScript Neues JavaScript
ID $('#x') document.getElementById('x') document.querySelector('#x')
Klasse $('.x') document.getElementsByClassName('x') document.querySelectorAll('.x')
Tag $('x') document.getElementsByTagName('x') document.querySelectorAll('x')
Attribut $('[x]') n/a document.querySelectorAll('[x]')
CSS (s.u.) $('x y') n/a document.querySelectorAll('x y')

Sowohl die jQuery-Methode $(…) als auch die JavaScript-Methoden .querySelector() / .querySelectorAll() unterstützen die Auswahl per CSS-Selektor. Damit können auch kompliziertere Suchen im DOM durchgeführt werden, wie z.B. mit nav a das Auffinden aller <a> in einem <nav>. Zum Glück ist die Browser-Unterstützung für .querySelector() / .querySelectorAll() inzwischen sehr gut.

Zu beachten ist, dass die JavaScript-Methoden .querySelector() / .querySelectorAll() je nach Browser um ein Mehrfaches langsamer sind als ihre „einfachen“ Geschwister .getElementById, .getElementsByClassName und .getElementsByTagName.

Mengenlehre: Selektieren in der Selektion

Interessanterweise kann jede Methode nicht nur auf das gesamte Dokument angewendet werden, sondern auf eine bereits bestehende Selektion. Damit kann die Suche stark beschleunigt werden.

In jQuery existiert dafür die .find()-Methode, die analog zu .on() funktioniert…

var navigation = $('nav');
var navLinks = navigation.find('a');
var navBolds = navigation.find('b');

…in regulärem JavaScript bleiben die Methoden identisch zu den für die Suche im Dokument verfügbaren Methoden:

var navigation = document.querySelector('nav');
var navLinks = navigation.querySelectorAll('a');
var navBolds = navigation.querySelectorAll('b');

Diese Methode kann sehr hilfreich sein, wenn später sowieso DOM-Manipulation an übergeordneten DOM-Elementen notwendig werden.

Zu beachten ist bei regulärem JavaScript, dass die Methoden .getElementById() und .querySelector() ein einzelnes Element bzw. einen einzelnen Node (d.h. ein DOM-Element) zurückgeben, während alle anderen Selektions-Methoden eine HTMLCollection bzw. NodeList zurückgeben, die vereinfacht gesagt ein Array von Nodes sind.

Event-Handler hinzufügen

Für das Hinzufügen von Event-Listenern bietet jQuery die Methode .on(), und JavaScript die Methode .addEventListener() an. (Wir ignorieren die Methoden zum Hinzufügen von Event-Handlern direkt via HTML-Attribut, da dadurch eine unglückliche Verkettung von Content (HTML) und Verhalten (JavaScript) entsteht.)

In beiden Fällen verfügt die Selektion über eine Methode, der man nur den Event-Typ und den eigentlichen Event-Listener übergeben muss.

$('nav').on('click', function() {
  $(this).addClass('active');
})

Der selbe Aufruf ist in Vanilla-JavaScript etwas mehr Schreibarbeit, aber ansonsten identisch:

document.querySelector('nav').addEventListener('click', function(event) {
  event.target.classList.add('active');
});

Zu beachten in JavaScript: Event-Listener können nur einem einzelnen DOM-Element hinzugefügt werden – jQuery erlaubt es, am Stück _mehreren DOM-Elementen ein und denselben Event-Listener hinzuzufügen.

In beiden Fällen steht im Event-Listener mit $(this) bzw. event.target das DOM-Element direkt zur Verfügung, auf dem das Event ausgelöst wurde. Voraussetzung ist bei JavaScript, dass der Event-Listener als ersten Parameter eine Variable namens event gesetzt bekommen hat.

Sieben Event-Typen auf einen Streich…

Ein nicht unwahrscheinlicher Anwendungsfall ist, verschiedene Event-Typen mit dem selben Event-Handler bedienen zu wollen. In jQuery kann man an die .on()-Methode eine Liste an verschiedenen Event-Typen übergeben:

// Fires on `click` `keyup` `blur`
$('nav').on('click keyup blur', function() {
  $(this).addClass('active');
})

In JavaScript ist ein bisschen mehr Gehirnschmalz notwendig, denn hier müssen wir jeden Event-Listener mit einem einzelnen Aufruf hinzufügen. Das könnte man in einer Schleife tun…

// Bad example: Fires on `click` `keyup` `blur`
['click', 'keyup', 'blur'].forEach(function(eventType) {
  document.querySelector('nav').addEventListener(eventType, function(event) {
    event.target.classList.add('active');
  });
});

…und handelt sich auf diese Weise zwei Performance-Killer ein: Einerseits wird in jedem Schleifendurchlauf document.querySelector neu ausgewertet, andererseits wird jedes Mal Speicher für eine neue, anonyme Funktion reserviert. Glücklicherweise kann man beide Konstruktionen aus dem Schleifenkörper herausziehen:

// Better example: Fires on `click` `keyup` `blur`
var eventTarget = document.querySelector('nav');
var eventListener = function(event) {
  event.target.classList.add('active');
};

['click', 'keyup', 'blur'].forEach(function(eventType) {
  eventTarget.addEventListener(eventType, eventListener);
});

Das Array abgerollt sieht dann sogar noch übersichtlicher aus, und zeigt plastisch den Vorteil der vorherigen Deklaration von Event-Ziel und -Listener:

// Best example for readability: Fires on `click` `keyup` `blur`
var eventTarget = document.querySelector('nav');
var eventListener = function(event) {
  event.target.classList.add('active');
};

eventTarget.addEventListener('click', eventListener);
eventTarget.addEventListener('keyup', eventListener);
eventTarget.addEventListener('blur', eventListener);

Weniger ist… weniger

Wenn sich auf einer Seite mehrere DOM-Objekte befinden, die wir mit einem identischen Event-Handler ausstatten wollen, so gibt es in jQuery die folgende Methode.

// Add Event Handler to all `.btn`
$('.btn').on('click', function() {
  $(this).addClass('active');
})

Besonders spannend: Bei .on() und .querySelectorAll() können auch mehrere CSS-Selektoren, durch Kommata getrennt, gleichzeitig abgefragt werden. Mit header a, footer a kriegt man eine Liste aller <a> in <header> und <footer> zurück.

In Vanilla-JavaScript wird das Hinzufügen zu Event-Listenern zu mehreren DOM-Elementen etwas umständlicher, weil ein Event-Handler immer nur einem DOM-Objekt hinzugefügt werden kann. Wenn wir also eine Liste von DOM-Objekten haben, müssen wir jedem DOM-Objekt beim Durchlaufen einer Schleife einen Handler verpassen:

// Add Event Handler to all `.btn`
document.querySelectorAll('.btn').forEach(function(btn) {
  btn.addEventListener('click', function(event) {
    event.target.classList.add('active');
  });
});

Warum ist das in JavaScript eigentlich so deutlich weniger bequem? Der Grund ist ganz einfach: Diese Konstruktion hat massive Performance-Auswirkungen. Wir fügen damit eine größere Anzahl von Event-Handlern hinzu, die alle das Gleiche tun, aber unterschiedliche DOM-Objekte beobachten müssen. Damit muss der Browser mehr Dinge beobachten. Bei einer 10×10 Zellen umfassenden Tabelle kann ein Event-Listener für eine Tabellenzelle also auf insgesamt 100 Events verteilt werden.

Für diesen Fall bietet jQuery eine performante Alternative: Statt jedes Element einzeln mit einem Event-Handler auszustatten, wird einfach ein im DOM oberhalb der zu beobachtenden DOM-Objekte ein DOM-Objekt ausgewählt. Dieses Objekt wird über alle Events informiert, die unterhalb von ihm stattfinden – das sogenannte „Event Bubbling“.

In jQuery wird der Filter für das eigentliche Event-Ziel als zusätzlicher Parameter von .on() übergeben.

// Add Event Handler to `nav`, fire if `.btn` was clicked
$('nav').on('click', '.btn', function() {
  $(this).addClass('active');
})

In Vanilla-JavaScript wird das Event-Ziel innerhalb des Event-Listeners gefiltert:

// Add Event Handler to `nav`, fire if `.btn` was clicked
document.querySelector('nav').addEventListener('click', function(event) {
  if (event.target.matches('.btn')) {
    event.target.classList.add('active');
  }
});

Bezogen auf unser Beispiel mit den 100 Tabellenzellen haben wir gerade aus 100 Event-Handlern einen einzigen Event-Handler gemacht. Das spart nicht nur Speicher, sondern erlaubt es auch, auf DOM-Elemente zu reagieren, die beim Hinzufügen des Event-Handlers noch gar nicht im DOM existierten. Wenn zum Beispiel zu unserem <nav>-Element erst nach dem Hinzufügen des Event-Handlers neue <… class=„btn“> zum Beispiel via AJAX hinzugefügt werden, wird unser Event-Handler auf dem <nav> auch diese Elemente mit verarbeiten, da er ja auf alle Elemente unterhalb von ihm reagiert.