Aaron said:
I jokingly say this is the late entry
Okay I have read all the event entry comments from John's Resig's
AddEvent comepition blog :-
http://ejohn.org/projects/flexible-javascript-events/
and put together the following offering for my LGPL'ed library
functions :-
Even if you only count posts to this group, there have been so many
variations of this code posted over so many years that the odds are
extremely good that this code is already either someone else's copyright
or public domain. Its only unique feature is the inappropriate use of
"cascade" where most have used 'capturing'.
function addEvent( el, type, fn, cascade) {
if ( el.addEventListener) {
cascade = cascade || false;
Why do this here? You could parenthesise the logical OR expression and
use it as the final argument in the method call, saving the assignment
operation and one scope chain resolution of - cascade -, without making
the result significantly harder to understand.
el.addEventListener( type, fn, cascade)
But then why provide the - cascade - parameter at all as whatever you do
IE will not process events during a capturing phase because it does not
have one? And the intrinsic event property branch will not capture at
all without explicit calls to - captureEvents - methods (which are not
provided by all (or even most) browsers, and are not available on all
DOM nodes even when provided.
What you have done by providing the - cascade - parameter to the
function is invite programmes using the function to experience radically
different behaviour between browsers, to make themselves aware of those
differences, to test the environment themselves to see which behaviour
is going to apply and to do a great deal of coding to accommodate them.
If they are going to have to do all of that your function, as a method
for attaching the listeners, is almost more trouble than it is worth as
the actual attaching of the listeners is trivial in comparison.
The more sensible alternatives are to provide a full event capturing
emulation for the second and third branches (which had been demonstrated
possible but is a huge bloat for the function) or to specify this method
as 'at target' and 'bubbling' phase only, forget the cascade parameter,
and just use a false boolean literal as the final argument to this
method call.
It has been the norm in cross-browser scripting for event handling to be
'at target' and 'bubbling' phase only precisely because IE has never
provided a capturing phase for events, so most people's experience,
examples and literature will be geared towards that type of event
handling.
}
else if ( el.attachEvent) {
el[type+fn] = function() {
Type-converting the - fn - function into a string and using that as part
of the property name under which a reference to this function is stored
is a staggeringly stupid thing to do.
For a start ECMA 262 specifies the result of the native function -
toString - method as "An implementation dependent representation of the
function", so you have no guarantee about what it may contain at all.
Secondly, there have been bugs in implementation - toString - methods in
the past, which highlights the possibility of bugs in the future. A
specific example was to be found in Opera's javascript prior to version
7. The bug what that all regular expression literals were serialised
as - [object] - by its - toString - method. The result would be that two
functions that differed only in terms of a regular expression literal
would produce identical string representations.
But even if you assume that all function - toString - methods will
return a string representation that is as unique as the function's
source code, and that no bugs exist in the environments in question, the
uniqueness of such a string is nowhere near related to the uniqueness of
the identity of the function object. And it is the identify of the
function object that is important for the - attachEvent - and -
detachEvent - methods.
Consider what happens if the functions being attached represent one of
the many closure based methods of associating a method call with a
particular javascript object instance. A simple example might be:-
function associateObjectWithEvent(obj, methodName){
return (function(ev){
return obj[methodName](ev, this);
});
}
(but any of those clunky 'bind' methods so beloved of library authors
would do just as well as an example). You might very well want to attach
multiple examples of the function returned from that function to the
same event handler of a single element, but they will all serialize as
the same string despite having unique identity. Attach two and then
attempt to remove the first and it will be the second that is removed,
while the first can never then be removed (and that is on IE, the
browser with memory leak issue).
fn.call( el, window.event);
}
el.attachEvent( 'on'+type, el[type+fn])
}
else
el[ 'on'+type] = fn
If this branch is ever good enough it is also always good enough.
Suggesting that the other two branches be removed entirely or that this
branch is not a suitable alternative to them. You invite as much
inconsistency with this as you did with your - cascade - parameter,
except this branch is the one that is least likely to be tested by the
programmer as modern browser will not tend to be entering this branch.
This means that the programmer may get the false impression that what
they are creating is cross-browser when in reality it is very likely to
behave unexpectedly in the event that the code encounters a browser that
will use this branch.
The - addEventListener - and - attachEvent - methods are all about
attaching multiple listeners to a single event on a single element. The
intrinsic event properties only allow the attaching of a single listener
to a single event on a single element. If you want an intrinsic event
property branch it should provide the full emulation of the other
methods and allow multiple factions to be attached. That can be done,
but again bloats the code, and once again brings into question the worth
of having the other two branches as once in place that single approach
would be as viable in all environments (and the absence of branching
would make the outcome much more consistent across browsers).
There is, of course, the question of cancelling default actions and
propagation. Browsers that only support intrinsic event properties may
be expecting default action cancelling to be achieved by returning false
from the function attached to the property, and may not allow the
cancelling of propagation at all. It takes quite a bit of work to
emulate the possibilities offered in the other branches for those
environments, and may mean imposing system-wide restrictions (like
forbidding propagation cancelling) in order to guarantee consistent
handling across browsers.
}
function removeEvent( el, type, fn, cascade) {
if ( el.removeEventListener) {
cascade = cascade || false;
el.removeEventListener( type, fn, cascade)
}
else if ( el.detachEvent) {
el.detachEvent( 'on'+type, el[type+fn])
el[type+fn] = null; // clear hash and IE memory leak
}
else
el[ 'on'+type] = null
}
Lessons :-
call W3C first to satisfy Opera and for common sence,
then MS as this is usually easy detectable, then legacy.
Based on 'Weisi Su' entry on
http://ejohn.org/projects/flexible-javascript-events/#comment-276560
and Michael White' suggestion using W3C first for correct
operation on Opera. plus legacy event handling added by me.
Which in the end was all very simular to some code I wrote
the previous day and forgot about
Added cascade parameter that defaults to bubble on W3C calls.
That was a mistake if you are not going to facilitate (emulate) a
capturing phase on browsers that don't use the W3C events DOM path.
Okay I have tried it on IE6 with and Drip and it does not seem
to produce and memory leaks AFAICS.
<snip>
Has drip been improved so that it can now detect closure based circular
references? It could not do that last time I looked at it. The
structures you create in the IE branch of this code will always produce
circular chains of references between DOM nodes and javascript objects,
and so you will only not provoke the issue if all listeners attached
with this method are later removed. Arranging that symmetrical handling
of listeners is another burden placed on the programmers using this
method. That is OK so long as they are told up-front that they will have
to address that issue themselves.
Well thats about it folks...any comments...holes...or suggesttions
are most welcome.
We will see. The biggest fault in your code is the inconsistent
interface it provides. Having the - cascade - parameter when it can only
influence the behaviour of some browsers is a bad idea. The fault of
trying to use the serialised function objects in the - addEvent - branch
is also significant. And the implication of the total, that the desired
outcome is a single general method, is also a mistake.
Recently I have been thinking about how to express what it is about the
attempt to be general that tends to results in code that bloated and
inefficient. Initially it occurred to me that being attempting to be
'general' in browser scripting has at least two dimensions; there are
the number of application contexts that are covered by the outcome, and
there are the number of browsers in which those application contexts are
accommodated.
These two dimensions can be confused so I should illustrate. In the
context here, and application context might be regarded as 'what you
intend doing', e.g.:-
1. You want to trigger some global code as the result of the user
activating (clicking or keyboard triggering) a button. You don't need to
know anything about the element that was activated, just what to do as a
result of its being activated.
2. You want the activation of a link to use methods of a series of
previously instantiated javascript objects to progressively create and
construct a query string that is added to the link's HREF before it is
used for navigation.
The first is very simple and does not require any preservation of -
this - references in attached handlers (as they are irrelevant) and does
not involve any 'binging' of javascript objects to functions. If it is
the most complex requirement of the system being built it can be
accommodated with extremely simple (and so by implication (in javascript
terms) fast) javascript code.
The second implies a need to reference the element activated and so
suggests that normalising the - this - references would be a good idea
(even if not essential (you could extract the element from the event
object's properties), it implies an ability to attach multiple listeners
to a single event and it requires a means of associating the listeners
with distinct javascript object instances.
Any method that accommodates the second will accommodate the first, but
at the cost of being bigger and slower (particularly if it is not going
to constrain the browser support dimension in the process). If a system
being created only needs the first to be accommodated then the code that
could accommodate the latter is over the top for that system.
You code cannot accommodate the latter because the need to bind
functions to javascript object instances would trip up the function
serialisation approach to function object identification. Thus you code
is more general that is needed for the first and not general enough for
the second, in the 'application contexts' dimension. The attempt to be
'general' usually ends up falling short of being truly general in that
dimension because the number of possible permutations can become so
great that they all cannot be accommodated behind a single API without
its becoming so ludicrously and obviously bloated that nobody would
consider using it. (As illustrated by the theoretical truly general
elopement position/dimension reporting method. Nobody has ever coded the
algorithm because the few people who appreciate what is involved realise
that executing 2000+ statements of code just to find out the page
relative x/y coordinates and width/height of an element is a
non-starter).
If the problem of attempting to be general is expressed as having two
dimension it is easy to why trying to extend the range of one of those
dimensions can have a disproportional impact on internal complexity,
bulk and performance. However, I have since realised that the attempt to
be 'general' has at least one more dimension.
There is a dimension of relevant knowledge, understanding and experience
(skills) of the people attempting to use any code that attempts to be
general. You will notice that one of arguments used to promote the
notion of general-purpose javascript libraries is that they allow
individual with relatively few relevant skills to achieve things that
would otherwise be beyond their abilities. And this is a direct
continuation of the copy-and-paste collections that have existed for
many years that (apparently, or superficially) allowed single
functionalities to be achieved by individuals through no more work than
pasting a block of code into an HTML document.
So where your code provides that - cascade - parameter, if properly
documented, a knowledgeable and experienced javascript programmer could
appreciate that if they wanted to use it the onus was on them to
accommodate the outcome. But to make the same API accommodate a wider
range possible 'programmers' that work would have to be moved inside
functions (so that the API's users did not have to face it). But that
would result in bloat, and it would result in a performance fall-off
(less so for W3C events DOM supporting browsers but pretty big for some
of the others).
With the ranges of at least three dimensions being expanded in the
pursuit of being 'general' the impact on internal complexity, bulk and
performance can be expected to be proportional to the cube of measure of
any signal dimension that is to be extended.
Richard.