protofunc()

jQuery: live-Methode / Braucht jQuery 1.4 eine neue API für Event Delegation?

Tags: deutsch, javascript, jquery

Jedes jQuery-Major-Release hat wesentliche Neuerungen/Verbesserungen gebracht. Bei jQuery 1.3 war es wohl die Einführung von Sizzle, die Umstellung von Browser-Sniffing auf Feature-Detection und die Einführung der live-Methode für Event Delegation.

Letztendlich habe ich die live-Methode in jQuery 1.3 so gut wie nie genutzt, da ich es für meine Usecases für ineffizient gehalten habe, das document-Objekt mit Eventlistener zu zuknallen. Eine präzisere Möglichkeit Event Delegation in jQuery 1.3 zu nutzen, war für mich immer die Nutzung der closest-Methode:

$('#nav').bind('click', function(e){
	// Der zweite Parameter wird erst in jQuery 1.4 eingeführt!
	var anchor = $(e.target).closest('a', this);
	if(!anchor[0]){return;)
	// mach was mit anchor
});

Das Problem mit der live-Methode

Mit jQuery 1.4 wird Event Delegation und die Möglichkeiten der live-Methode stark erweitert. Zum einen werden Events unterstützt, die nicht bubbeln und zum anderen soll der Entwickler bestimmen können, an welches DOM-Objekt der Eventlistener hinzugefügt wird. Dies dürfte in jedem Fall dazu führen, daß die Methode häufiger eingesetzt wird. Gleichzeitig haben einige Leute in der Vergangenheit gefordert, die live-Mwthode entweder abzuändern oder zumindest eine neue Methode einzuführen. Dies geschah mit besonderer Rücksicht auf Performance und wurde – meiner Meinung nach – zurecht abgelehnt.

Das Problem ist jedoch, daß die live-Methode schwer zu erklären/dokumentieren ist, leicht zu Fehlern führen kann und den Entwickler dazu zwingt bereits beim Instantiieren eines jQuery-Objekts über die spätere Art der Event Delegation nachzudenken. Diese Grundproblematik läßt sich bereits bei jQuery 1.3 ausmachen und wird bei jQuery 1.4 noch komplizierter (aber nicht unbedingt schlimmer).

Problem bei jQuery 1.3

Laut jQuery-Dokumentation kann live u.a. nicht verwendet werden, wenn man beim Instanzieren den Context-Parameter nutzt bzw. wenn man Traversing-Methoden zwischenschaltet. Eine ältere jQuery Dokumentation hat hiervon ausdrücklich die Methode find als nicht funktionierende Traversing Methode ausgenommen. Letztendlich stimmt diese Behauptung in der Dokumentation nicht ganz. Eine Erklärung wann dies funktionieren kann und wann nicht, wäre jedoch einfach zu kompliziert. Hier eine kurze Erklärung

// funktioniert in 1.3 nicht,
$('li', $('#nav')[0]).live('click'....
// ... da
// 1. der Eventlistener dem document-Objekt hinzugefügt wird
// 2. jQuery ausschließlich den 'li', aber nicht den '#nav' Selektor kennt

// funktioniert in 1.3,
$('li', $('#nav')).live('click'....
$('#nav').find('li').live('click'....
// ... da
// jQuery beide Selektor-Bestandteile kennt und diese zu '#nav li' zusammenfassen kann

// funktioniert in 1.3 nicht,
$('li, a', $('#nav')).live('click'....
// ... da jQuery beide Selektor-Bestandteile zu '#nav li, a' zusammenfaßt

Eskalierung des Problems mit jQuery 1.4 (nightly-Stand)

jQuery 1.4 ist noch nicht draußen, aber der derzeitige Stand kann über github eingesehen werden. Zudem existiert eine öffentliche Alpha, welche ich zum Testen herangezogen habe.

Mit dem derzeitigen Stand von jQuery 1.4a1 wird die oben beschriebene Ungenauigkeit zu einem großen Problem, denn die Verwendung des context-Parameters soll nicht nur ermöglicht werden, sondern wird zu einem elementaren Feature, da nun der context-Parameter bestimmen soll, an welchem Element der Eventhanlder gebunden werden soll. Das Problem hierbei ist jedoch eben die Tatsache, daß es eine Reihe von Bedingungen auftreten müssen, damit das ganze funktioniert.

//Event wird in der Regel an nav-Element gebunden und funktioniert in der Regel wie gewünscht
$('li', $('#nav')[0]).live('click'....
// aber dann nicht, wenn $('#nav')[0] === undefined ist

//Event wird an document-body gebunden (Obwohl ein context-Parameter angegeben wurde, funktioniert aber ansonsten wie gewünscht.)
$('li', $('#nav')).live('click'....

//Funktioniert wie bei jQuery 1.3 nicht wie gewünscht
$('li, a', $('#nav')).live('click'....

Die Dokumentation müßte hier einerseits auf die Unterscheidung hinweisen, was alleine für sich bereits ziemlich verwirrend sein könnte und andererseits darauf, daß die 1. Zeile fehleranfällig ist. Kommt nämlich auf der gesamten Seite kein #nav-Element vor, ist $(‘#nav’)[0] undefined, was dazu führt, daß erstens der Eventhandler zum document.body hinzugefügt und zweitens der Eventhandler bei jedem Click auf/in irgend ein li aufgerufen wird und damit nicht so funktioniert wie gewünscht.

Das gesamte Problem wird zusätzlich verschärft, wenn man einen Eventhandler mehreren Elementen hinzufügen möchte.

//Event wird an das document.body gebunden und funktioniert wie gewünscht
$('div.teaser', $('div.teaser-wrapper')).live('click'...

//Schreibweise wird von jQuery nicht unterstützt, Event wird an document.body gebunden und jeder Click auf/in ein div.teaser löst Event aus
$('div.teaser', $('div.teaser-wrapper').get()).live('click'...

Funktionierender Code, der das neue Event Delegation-Feature von jQuery 1.4 nutzt und dabei sowohl klar, verständlich als auch robust ist, könnte beispielsweise wie folgt aussehen:

$('#nav').each(function(){
	$('a', this).live('click' ...
});
//oder
$('div.teaser-wrapper').each(function(){
	$('div.teaser', this).live('click' ...
});

Letztendlich sind wir damit ziemlich nah an dem dran, was wir bereits in jQuery 1.3 schreiben können, um Event Delegation mit anderem context als document bzw. document.body zu nutzen, nämlich:

$('#nav').bind('click', function(e){
	// Der zweite Parameter wird erst in jQuery 1.4 eingeführt!
	var anchor = $(e.target).closest('a', this);
	if(!anchor[0]){return;)
	// mach was mit anchor
});

Es ist ziemlich “jQuery-unlike” mehrere Zeilen-Code zu schreiben, um nur eine “einfache” Sache zu erreichen, aber genau das würde bei einer Beibehaltung der derzeitigen API passieren. Ganz abgesehen davon, daß die Dokumentation um weitere kompliziertere Erklärungen nicht umhin kommen würde.

Eine einfache Implementierung einer neuen jQuery-Event Delegation API könnte hierbei beispielsweise wie folgt aussehen.

(function($){
	var dummy = $([]);

	$.each({addLive: 'live', removeLive: 'die'}, function(name, jMethod){
		$.fn[name] = function(sel){
			var args = (this[0]) ? Array.prototype.slice.call(arguments, 1) : [];
			return this.each(function(){
				dummy.selector = sel;
				dummy.context = this;
				$.fn[jMethod].apply(dummy, args);
			});
		};
	});
})(jQuery);

Die Nutzung würde hierbei wie folgt aussehen:

function fn(){
	alert('F');
}
//bind live:
$('div.teaser-wrapper').addLive('div.teaser', 'click', fn);
// unbind live/die
$('div.teaser-wrapper').removeLive('div.teaser', 'click', fn);

Fazit:

jQuery 1.4 benötigt nicht unbedingt eine neue Methode für Event-Delegation. Die Verwendung des context-Parameters wird nun grundsätzlich ermöglicht und damit mögliche Fehler einer flaschen Benutzung minimiert. “Power User”, die jedoch Wert darauf legen, daß sie die volle Kontrolle darüber haben, welchem Element der Event-Listener genau hinzugefügt wird, würden sich jedoch stark über eine anders funktionierende Methode freuen.

Eine solche Methode wäre dann nicht nur einfacher zu erklären, sondern würde ebenfalls die Forderung nach einer performanteren (ohne Unnötiges Selektieren von Elementen, die man nicht braucht) berücksichtigen.

Written December 8, 2009 by
protofunc