CogitoWeb

3 luglio 2010

addEvent: una (altra) soluzione / a(nother) solution

Filed under: JavaScript — MaxArt @ 14:36
Tags: ,
English Version English version 

Una delle differenze più importanti tra il codice JavaScript che si scrive per Internet Explorer e quello per un browser aderente alle direttive W3C consiste nei metodi che si usano per definire un evento su un elemento della struttura della pagina. Mentre Internet Explorer usa i metodi attachEvent e removeEvent, gli standard indicano la strada in addEventListener e removeEventListener, rispettivamente, come funzioni per aggiungere e togliere un evento. Al di là delle differenze di funzionamento degli eventi tra i browser (Internet Explorer non supporta l’event capturing, motivo per il quale, del resto, è una tecnica poco sfruttata), i due metodi fanno pressoché la stessa cosa. Ma questo non vuol dire che fanno la stessa cosa.

Quello che si vorrebbe, magari, è una funzione che possa utilizzare l’uno o l’altro metodo in automatico a seconda del supporto che viene fornito dal browser. Una soluzione largamente adottata fu fornita da Scott Andrew LePera oltre 9 anni fa:

function addEvent(obj, evType, fn, useCapture){
  if (obj.addEventListener){
    obj.addEventListener(evType, fn, useCapture);
    return true;
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be attached");
  }
}

Questa semplice soluzione, insieme con la controparte removeEvent, funziona discretamente fino ad un certo punto. La differenza tra addEventListener e attachEvent consiste nel fatto che, nella funzione passata come gestore dell’evento, secondo gli standard la parola chiave this deve riferirsi, come logica vorrebbe, all’elemento HTML su cui è definito l’evento, ma in Internet Explorer this fa riferimento all’oggetto window. Una bella seccatura!

Un primo approccio alla soluzione

Una soluzione consiste nell’usare un wrapping del gestore dell’evento nel caso di Internet Explorer:

...
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, function() {fn.call(obj)});
  ...

Grazie all’uso di call, siamo sicuri che this all’interno della funzione farà riferimento all’elemento su cui è definito l’evento e non all’oggetto window. Basta questo? Macché! Questo può bastare finché gli eventi così definiti non debbano essere in seguito eliminati con removeEvent, perché il codice relativo è questo:

  obj.detachEvent("on"+evType, fn);

Ebbene, la rimozione dell’evento non funzionerà, perché si cercherà di rimuovere la funzione fn come gestore, ma non è tale funzione ad essere stata definita come gestore dell’evento, bensì il suo wrapper function() {fn.call(obj)}! Dovremmo quindi dare il riferimento al wrapper della funzione, ma questo ci è impossibile una volta usciti dalla funzione addEvent.

L’idea di John Resig

A tale proposito, il guru Peter-Paul Koch, autore del fenomenale (per quantità di risorse) sito quirksmode.org, indispensabile per aiutare tutti gli sviluppatori del web a districarsi tra le differenze di comportamento dei più comuni browser, nel 2005 indisse un concorso per definire un’accoppiata di funzioni addEvent/removeEvent che risolvesse la questione di this in Internet Explorer.

Il concorso fu vinto da un altro guru, John Resig, l’autore della notissima libreria jQuery. Questo è il codice che ha usato:

function addEvent( obj, type, fn ) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
    obj.attachEvent( 'on'+type, obj[type+fn] );
  } else
    obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
  if ( obj.detachEvent ) {
    obj.detachEvent( 'on'+type, obj[type+fn] );
    obj[type+fn] = null;
  } else
    obj.removeEventListener( type, fn, false );
}

Comincio col dire che la soluzione, ovviamente, funziona benissimo. Anche in questo caso si va costruire un wrapper della funzione originale (obj['e'+type+fn]), che viene però richiamato come funzione locale. Ho però alcune osservazioni a riguardo:

  • le funzioni eseguono un controllo sulla presenza di attachEvent/detachEvent ogni volta che vengono chiamate: questo è superfluo, perché è chiaro se tali metodi saranno presenti una volta, lo saranno sempre (non è che il motore JavaScript cambia nel bel mezzo dell’esecuzione!)
  • in ogni caso viene effettuato prima un controllo su attachEvent e non su addEventListener, come sarebbe più logico (prima gli standard);
  • mi è sempre parso un metodo piuttosto invasivo, che “sporca” l’elemento HTML con la definizione di attributi (perché così vengono considerati da Internet Explorer i membri definiti gli elementi HTML) con nomi assurdamente lunghi, con garanzia di unicità non completa e per giunta non vengono ripuliti del tutto da removeEvent.

Si tratta, si badi, di argomentazioni abbastanza secondarie, se non la prima che può avere un certo marginale impatto prestazionale. Tuttavia, credo che si possano risolvere in toto.

Una nuova soluzione

Questa è l’alternativa che propongo:

if (document.addEventListener) {
  addEvent=function addEvent(obj, type, fn) {obj.addEventListener(type, fn, false);}
  removeEvent=function removeEvent(obj, type, fn) {obj.removeEventListener(type, fn, false);}
} else {
  addEvent = function addEvent(obj, type, fn) {
    fn[type+fn] = function() {return fn.call(obj, window.event);};
    obj.attachEvent("on"+type, fn[type+fn]);
  };
  removeEvent = function removeEvent(obj, type, fn) {obj.detachEvent("on"+type, fn[type+fn]);}
}

La differenza con la soluzione di John Resig sta nel fatto che la mia versione di addEvent va a salvare il riferimento al wrapper non sull’oggetto elemento HTML, ma sull’oggetto fn che viene passato come argomento. In questo modo, removeEvent sa sempre dove andare a prendere il wrapper e rimuoverlo.

Posso dire di essere piuttosto sorpreso di aver sviluppato qualcosa che mi sia piaciuto più della versione di un mostro sacro come John Resig, ed ogni tanto mi chiedo se ciò che ho scritto non abbia delle controindicazioni a me non visibili. Finora, però, non m’è parso.


addEvent: a(nother) solution


One of the main differences between the JavaScript code you write per Internet Explorer and the one for a W3C standard compliant browser is about the methods you use to define an event on an element of the page structure. While Internet Explorer uses the methods attachEvent and removeEvent, the standards have their way in addEventListener and removeEventListener, rispectively, ad function to add and remove an event. Leaving apart the working differences among browsers (Internet Explorer doesn’t support event capturing, and that’s actually the reasing why this technique is not used often), these two methods almost do the same thing. But this doesn’t mean that they do the same thing.

What we actually want is a function that calls one method or the other automatically, depending on the browser support. A widely used solution was given by Scott Andrew LePera more than 9 years ago:

function addEvent(obj, evType, fn, useCapture){
  if (obj.addEventListener){
    obj.addEventListener(evType, fn, useCapture);
    return true;
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be attached");
  }
}

This simple solution, together with the removeEvent counterpart, works fairly well under certain conditions. The difference between addEventListener and attachEvent is in the fact that, while running the function passed as event listener, by the standards the key word this must refer, as logic should hint, to the HTML element on which the event is defined, but on Internet Explorer this always refers to the window object. A real pain!

<h3A first approach to the solution

A solution consists of the using of a wrapper of the event listener in the Internet Explorer case:

...
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, function() {fn.call(obj)});
  ...

Thanks to call, we’re sure that this inside the function will refer to the element on which the event is defined and not to the window object. Is that enough? Not at all! This can be enough until the events defined in this way are not to be removed later by removeEvent, because this is the relative part:

  obj.detachEvent("on"+evType, fn);

Therefore, removing the event will not work, because it will try to remove the function fn as the listener, but that not the function defined as the event listener, but rather it’s its wrapper function() {fn.call(obj)}! So we should give the reference to the function wrapper, but that’s impossible once we’ve returned from the addEvent function.

John Resig’s idea

By this way, guru Peter-Paul Koch, author of the fantastic (for resource quantity) site quirksmode.org, essential to help web developers to have their way among behaviour differences of the most common browsers, in 2005 called a contest to define an addEvent/removeEvent functions couple to solve the this problem in Internet Explorer.

Not surprisingly, the contest was won by another guru, John Resig, author of the arch-famous library jQuery. This is the code he used:

function addEvent( obj, type, fn ) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
    obj.attachEvent( 'on'+type, obj[type+fn] );
  } else
    obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
  if ( obj.detachEvent ) {
    obj.detachEvent( 'on'+type, obj[type+fn] );
    obj[type+fn] = null;
  } else
    obj.removeEventListener( type, fn, false );
}

Let’s start to say that the solution, obviously, works just fine. In this case, too, we build a wrapper of the original function (obj['e'+type+fn]), which is instead called as a “local” method. I have some notes about it:

  • those functions checks the presence of addEvent/removeEvent every time they’re called: this is redundant, because it’s obvious that if those methods are definded once, they always are (you don’t change the JavaScript engine in the middle of the execution!)
  • anyway, attachEvent is checked first, not addEventListener, as logic would suggest (standards first);
  • it always seemed to me an obtrusive way, that “spoils” the HTML element defining attributes (because it’s the way Internet Explorer deals with members defined on HTML elements) with ridicously long names, without the complete uniqueness warranty and, moreover, not entirely cleaned by removeEvent.

These are, mind you, quite minor arguments, with the exception of the first one which can have some marginal performance influence. Anyway, I think that they can be completely solved.

A new solution

This is the alternative I propose:

if (document.addEventListener) {
  addEvent=function addEvent(obj, type, fn) {obj.addEventListener(type, fn, false);}
  removeEvent=function removeEvent(obj, type, fn) {obj.removeEventListener(type, fn, false);}
} else {
  addEvent = function addEvent(obj, type, fn) {
    fn[type+fn] = function() {return fn.call(obj, window.event);};
    obj.attachEvent("on"+type, fn[type+fn]);
  };
  removeEvent = function removeEvent(obj, type, fn) {obj.detachEvent("on"+type, fn[type+fn]);}
}

The difference with the solution of John Resig is that my version of addEvent saves the wrapper reference not on the HTML element but rather on the fn object that is passed as argument. This way, removeEvent always knows where to get the wrapper and remove it.

I must say I’m quite surprised to have developed something that I liked more than the version of a sacred genius as John Resig, and sometimes I wonder if what I wrote has some issues that I don’t see. It didn’t seem to me… so far.

Annunci

Crea un sito o un blog gratuitamente presso WordPress.com.