Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Any need for addEventListener() and attachEvent() at all?

74 views
Skip to first unread message

Peter Michaux

unread,
Nov 16, 2006, 5:10:42 PM11/16/06
to
Hi,

Today I have been testing the event models from Netscape 4.8 and IE 4
to the current crop of browsers. I'd like to write a small event
library similar in purpose to the Yahoo! UI event library but with less
features and code. The Yahoo! event library is one of the best
libraries in YUI but it still seems to me to have some confused
code...that or I'm still confused.

The Yahoo! UI library focuses on using addEventListener and
attachEvent. However, due to the click and dblclick bugs in Safari a
long legacy event workaround is included to use a Netscape4-type event
model for Safari. Something like this

var listeners = [function(event){}, function(event){}];
document.getElementById('blue').onmouseover = function(event) {
for (var i=0; i<listeners.length; i++) {
listeners[i](event);
}
};

With this above example, multiple handler functions can be fired for a
single event. I imagine that this is an old trick that has been around
for a long time, yes?

With all the new browsers I tested with this legacy workaround, the
listener handlers can use event.stopPropogation() or
event.cancelBubble=true and they work as desired. The handler functions
can also use event.preventDefault() and event.returnValue=false and
they too work. These seem to work because the event object passed to
the handlers is a modern event object and not one from Netscape4.

My question is, if Safari needs this legacy workaround, and the legacy
workaround seems to work in all the browsers that have addEventListener
or attachEvent, then why bother with the addEventListener and
attachEvent functions at all? Why not just use the legacy way for all
browsers and all type of events.?

Thank you,
Peter

David Golightly

unread,
Nov 17, 2006, 2:16:40 PM11/17/06
to
I know what you're saying. I'd say for most basic, simple
event-handling tasks, DOM Level 0 (element.onclick etc.) are perfectly
fine, and the hack you mention for adding multiple events is very
useful and for most purposes will suffice. However, as I understand
it, there are still some advantages to DOM Level 2:

1. Event capturing (optional): You can choose to pre-empt other
handlers that might be in line to listen to this event by using event
capturing.

2. De-registering events: When writing widgets, I often have different
modules adding event handlers to the same elements, and using DOM Level
2, they don't have to know about each other. De-registering an single
listener is much easier using the Level 2 DOM.

Of course, one of the problems with DOM Level 2 is that, as you pointed
out, for some reason - god only knows why - IE doesn't support it, but
has instead an alternate nonstandard event model.

I wrote an Events module a while back that I've been employing in
production sites for a while now, and it's been working pretty well for
me. One thing this module does is help ensure cleanup of registered
event handlers. Just put Events.clear() in your window.onload handler
and any events you registered through this will be cleaned up, fixing
the IE closure memory leak problem (at least for these handlers):

var Events = {};

Events.register = {};

Events._addToRegister = function(node, type, fn, capt) {
// DOM node gets converted into a string; we keep an array for
// events of similar DOM nodes, and also a separate reference
// to the node itself
if (!Events.register[node]) Events.register[node] = [];
Events.register[node].push({'node': node, 'type': type, 'fn': fn,
'capt': capt});
};

Events._removeFromRegister = function(node, type, fn) {
// Remove from cached register
if (node in Events.register) {
var r = Events.register[node];
for (var i=0; i<r.length; i++) {
if (r[i].type == type && r[i].fn == fn) {
var e = Events.register[node].splice(i, 1)[0];
if (r.length == 0) delete Events.register[node];
return e;
}
}
}
};

// De-registers all events
Events.clear = function(node) {
if (node) {
var n = Events.register[node];
for (var i=0; i<n.length; i++) {
if (n[i].type == 'unload') continue;
Events.remove(n[i].node, n[i].type, n[i].fn);
}
} else {
for (var node in Events.register) {
Events.clear(node);
}
}
};

// Browser-specific implementations
if (document.addEventListener) {
Events.add = function(node, type, fn, capt) {
capt = capt || false;
node.addEventListener(type, fn, capt);
this._addToRegister(node, type, fn, capt);
};
} else if (document.attachEvent) {
Events.add = function(node, type, fn) {
node.attachEvent('on'+type, fn);
this._addToRegister(node, type, fn);
};
} else {
Events.add = function(node, type, fn) {
node['on'+type] = fn;
};
}


if (document.createEvent) {
Events.create = function(type, data, eventClass) {
eventClass = eventClass || 'Event';
var e = document.createEvent(eventClass);
e.initEvent(type, true, false);

e.datatype = type;
e.data = data;
return e;
};
} else if (document.createEventObject) {
Events.create = function(type, data) {
var e = document.createEventObject();

e.datatype = type;
e.data = data;
return e;
};
} else Events.create = function() {return null; };


if (document.dispatchEvent) {
Events.fire = function(node, e) {
node.dispatchEvent(e);
};
} else if (document.fireEvent) {
Events.fire = function(node, e, type) {
node.fireEvent(type, e);
};
}


if (document.removeEventListener) {
Events.remove = function(node, type, fn, capt) {
if (!node.removeEventListener) {
Events._removeFromRegister(node, type, fn);
return;
}
var e = Events._removeFromRegister(node, type, fn);
if (e) node.removeEventListener(type, fn, e.capt);
};
} else if (document.detachEvent) {
Events.remove = function(node, type, fn) {
try {
node.detachEvent('on' + type, fn);
} catch (ex) {}
Events._removeFromRegister(node, type, fn);
};
} else {
Events.remove = function(node, type, fn) {
node['on'+type] = null;
delete node['on'+type];
};
}

There's also fallback support for DOM Level 0 event handling, but
perhaps a solution like the one Yahoo uses would be a nice touch. In
general, though, I'm not too worried about older browsers in my own
code.

Peter Michaux

unread,
Nov 17, 2006, 5:23:33 PM11/17/06
to
Hi David,

Thanks for the reply.

David Golightly wrote:
> I know what you're saying. I'd say for most basic, simple
> event-handling tasks, DOM Level 0 (element.onclick etc.) are perfectly
> fine, and the hack you mention for adding multiple events is very
> useful and for most purposes will suffice. However, as I understand
> it, there are still some advantages to DOM Level 2:
>
> 1. Event capturing (optional): You can choose to pre-empt other
> handlers that might be in line to listen to this event by using event
> capturing.

The IE event model natively supports event bubbling. An event library
could be written to take control of all event capture and bubbling and
thereby give event caputure to IE. I think I can get by fine without
event capture so this advantage of DOM Level 2 doesn't sway me much
now. Of course if event handling is wrapped in a library then later
capture support could be added so that IE can do it too.

> 2. De-registering events: When writing widgets, I often have different
> modules adding event handlers to the same elements, and using DOM Level
> 2, they don't have to know about each other. De-registering an single
> listener is much easier using the Level 2 DOM.

I agree it is much easier. But given that any event library that will
support Safari will have to have legacy events, then the pain of
de-regestering individual handlers from an array of handlers will have
to be included.

> I wrote an Events module a while back that I've been employing in
> production sites for a while now, and it's been working pretty well for
> me. One thing this module does is help ensure cleanup of registered
> event handlers. Just put Events.clear() in your window.onload handler
> and any events you registered through this will be cleaned up, fixing
> the IE closure memory leak problem (at least for these handlers):

<snip code>

Thanks for posting the code example. It is always interesting to see
other people's code.

> if (!Events.register[node]) Events.register[node] = [];

Does using node like this really create a unique string for a node? If
two nodes are the same, ie identical list elements in the same list
will this work?

> // Browser-specific implementations
> if (document.addEventListener) {
> Events.add = function(node, type, fn, capt) {
> capt = capt || false;
> node.addEventListener(type, fn, capt);
> this._addToRegister(node, type, fn, capt);
> };

In some versions of Safari, this won't work for click events where you
call preventDefault(). This also won't allow you to attach dblclick
handlers in some Safari. These are the problems that made me start
thinking using only DOM0 might be the way to go.


> } else if (document.attachEvent) {
> Events.add = function(node, type, fn) {
> node.attachEvent('on'+type, fn);
> this._addToRegister(node, type, fn);
> };
> } else {
> Events.add = function(node, type, fn) {
> node['on'+type] = fn;
> };
> }

This doesn't allow multiple handlers to be assigned to an element with
the DOM0 fallback. This could lead to unexpected behavior as handlers
overwrite each other. This is one advantage of using the Yahoo! UI
library where you can attach multiple handlers using the DOM0 approach.
This also means they can workaround the Safari problems.

<snip of synthetic event code>

Do you find you create synthetic events often? I haven't used them at
all but can imagine automated testing could use them well.

> I'm not too worried about older browsers in my own code.

Fair enough. I would really like to write a robust event library that
just works and doesn't suprise me some day down the road. It could be a
pipe dream :)

What I'm wondering is if the DOM0 implementations in the browsers have
any bugs like the DOM2/Safari bugs.

Thanks again,
Peter

David Golightly

unread,
Nov 18, 2006, 5:04:54 PM11/18/06
to
> > if (!Events.register[node]) Events.register[node] = [];
>
> Does using node like this really create a unique string for a node? If
> two nodes are the same, ie identical list elements in the same list
> will this work?

No, it doesn't - I suppose you could somehow hash the node, but I don't
bother here. What you get instead is the node name converted into a
string (from Object.toString), something like [object HTMLDivElement].
But this is why this string indexes an array of nodes, not a single
node. So all your divs with registered event handlers will be under an
array Events.register['[object HTMLDivElement]']

> > // Browser-specific implementations
> > if (document.addEventListener) {
> > Events.add = function(node, type, fn, capt) {
> > capt = capt || false;
> > node.addEventListener(type, fn, capt);
> > this._addToRegister(node, type, fn, capt);
> > };
>
> In some versions of Safari, this won't work for click events where you
> call preventDefault(). This also won't allow you to attach dblclick
> handlers in some Safari. These are the problems that made me start
> thinking using only DOM0 might be the way to go.

Yeah, I haven't had a chance to test this on Safari. Seems like a lot
of their DOM Level 2 implementation is incomplete.

> > } else if (document.attachEvent) {
> > Events.add = function(node, type, fn) {
> > node.attachEvent('on'+type, fn);
> > this._addToRegister(node, type, fn);
> > };
> > } else {
> > Events.add = function(node, type, fn) {
> > node['on'+type] = fn;
> > };
> > }
>
> This doesn't allow multiple handlers to be assigned to an element with
> the DOM0 fallback. This could lead to unexpected behavior as handlers
> overwrite each other. This is one advantage of using the Yahoo! UI
> library where you can attach multiple handlers using the DOM0 approach.
> This also means they can workaround the Safari problems.

That's why I said I should look at the Yahoo code and incorporate some
of their algorithms. :)

>
> <snip of synthetic event code>
>
> Do you find you create synthetic events often? I haven't used them at
> all but can imagine automated testing could use them well.

I do actually create synthetic events; I have for example a method I
didn't list before:

Events.click = function(node) {
if (node.click) node.click();
else {
var e = Events.create('click', null, 'MouseEvents');
e.initMouseEvent('click', true, true, window,
0, 0, 0, 0, 0, false, false, false, false, 0, null);
Events.fire(node, e, 'click');
}
};

because Firefox (argh) doesn't implement Node.click()


>
> > I'm not too worried about older browsers in my own code.
>
> Fair enough. I would really like to write a robust event library that
> just works and doesn't suprise me some day down the road. It could be a
> pipe dream :)

Yeah, I guess that's why I included the DOM 0 fallback. (Which could
be robustified by implementing the Yahoo algorithm.) The idea is you
have a good enough library that wraps all the browser inconsistencies
of all time; you develop this library with unit tests, and test &
refine on all browsers, then you just use that library for all your
apps and stop worrying about browser inconsistencies for every app you
develop. I share your vision!


>
> What I'm wondering is if the DOM0 implementations in the browsers have
> any bugs like the DOM2/Safari bugs.
>

I wonder if you couldn't just implement the functionality of the DOM
Level 2 events module in JS - it looks like most of it's possible.
(Though it most certainly wouldn't be as fast as native support, where
available.) As for DOM 0 support, DOM 0 is ancient already and its
support is well-documented. Sure, there are browsers with proprietary
events (IE has a ton of them) and inconsistencies with the "this"
keyword within an event handler. But this is a well-known model, so I
don't see why you couldn't just fall back on this if performance isn't
critical.

Peter Michaux

unread,
Nov 18, 2006, 8:35:46 PM11/18/06
to
David Golightly wrote:

> Peter Michaux wrote:
> > I would really like to write a robust event library that
> > just works and doesn't suprise me some day down the road.
>
> Yeah, I guess that's why I included the DOM 0 fallback. (Which could
> be robustified by implementing the Yahoo algorithm.) The idea is you
> have a good enough library that wraps all the browser inconsistencies
> of all time; you develop this library with unit tests, and test &
> refine on all browsers, then you just use that library for all your
> apps and stop worrying about browser inconsistencies for every app you
> develop. I share your vision!

Here is my rough draft of the library. If anyone is interested please
take a look and let me know what you think.

http://peter.michaux.ca/temp/event.html

I need to add many more unit tests and docs but I think that's enough
for today.


> > What I'm wondering is if the DOM0 implementations in the browsers have
> > any bugs like the DOM2/Safari bugs.
>
> I wonder if you couldn't just implement the functionality of the DOM
> Level 2 events module in JS - it looks like most of it's possible.
> (Though it most certainly wouldn't be as fast as native support, where
> available.)

I don't know if the performance would be noticably different even in a
dragdrop situation. After any bugs are out of the library above then
performance tests can be made.

Peter

David Golightly

unread,
Nov 24, 2006, 2:30:59 AM11/24/06
to

Hey Peter,

Your code looks good and runs well, but I'm still a bit concerned about
performance here. In the example test cases you have, there's not a
problem, but consider your code here:

el['on' + type] = function(e) {
e = e || window.event;
var listeners =
arguments.callee.listeners;
for (var i=0, len=listeners.length; i<len;
i++) {
var l = listeners[i];
if (l) {l.wrappedFn(e);}
}
};

Here is the relevant W3C recommendation
(http://www.w3.org/TR/DOM-Level-2-Events/events.html):

quote
Any exceptions thrown inside an EventListener will not stop propagation
of the event. It will continue processing any additional EventListener
in the described manner.
/quote

I was concerned about your implementation's synchronous implementation;
however, it appears that the standard is not clear on whether multiple
events should be called synchronously or asynchronously. (I believe
the FF is asynchronous for multiple events, which would improve
performance considerably. Need test to determine whether this is the
case.)

However, what your implementation is missing is fallback error
handling. If an error occurs in one handler it will prevent the rest
of the handlers from executing.

That's all. Good work otherwise.

-David

Peter Michaux

unread,
Nov 24, 2006, 2:55:37 AM11/24/06
to
Hi David,

David Golightly wrote:
> In the example test cases you have, there's not a
> problem, but consider your code here:
>
> el['on' + type] = function(e) {
> e = e || window.event;
> var listeners = arguments.callee.listeners;
> for (var i=0, len=listeners.length; i<len; i++) {
> var l = listeners[i];
> if (l) {l.wrappedFn(e);}
> }
> };
>
> Here is the relevant W3C recommendation
> (http://www.w3.org/TR/DOM-Level-2-Events/events.html):
>
> quote
> Any exceptions thrown inside an EventListener will not stop propagation
> of the event. It will continue processing any additional EventListener
> in the described manner.
> /quote

This is a good point but why exactly would one of my handlers throw an
error :)

I was thinking there must be some advantages of using the W3C functions
but then I keep think that if Safari is one of the target browser then
I must make it run just as well on Safari. This is a program to the
lowest common denominator approach.


> I was concerned about your implementation's synchronous implementation;
> however, it appears that the standard is not clear on whether multiple
> events should be called synchronously or asynchronously. (I believe
> the FF is asynchronous for multiple events, which would improve
> performance considerably. Need test to determine whether this is the
> case.)

JavaScript is single threaded. Doesn't that mean only one handler can
run at a time?


> However, what your implementation is missing is fallback error
> handling. If an error occurs in one handler it will prevent the rest
> of the handlers from executing.

I imagine a try-catch around call to each handler would take care of
this.

-------------

The other option is to figure out how to detect the Safari browsers
that have the click and dblclick event problems and push them down the
degredation path as though they are ancient browsers. Then the path
would be clear to use the W3C functions for all other browsers with
addEventListener().

Thanks for the feedback.

Peter

Peter Michaux

unread,
Nov 24, 2006, 3:17:50 AM11/24/06
to
David Golightly wrote:
>
> quote
> Any exceptions thrown inside an EventListener will not stop propagation
> of the event. It will continue processing any additional EventListener
> in the described manner.
> /quote

I just verified that IE6, Safari 2, Firefox 1.5 and Opera 9 really do
this. When an error is thrown in one handler it seems like the other
handlers still run.

> I was concerned about your implementation's synchronous implementation;
> however, it appears that the standard is not clear on whether multiple
> events should be called synchronously or asynchronously. (I believe
> the FF is asynchronous for multiple events, which would improve
> performance considerably. Need test to determine whether this is the
> case.)

How could this be checked? I just tried a few big loops in one handler
to keep it running and see if the other handler ran at the same time.
That didn't give any good information.

When would a handler really need to run for such a long period of time?

Peter

David Golightly

unread,
Nov 24, 2006, 1:21:45 PM11/24/06
to

Peter Michaux wrote:
> David Golightly wrote:
> >
> > quote
> > Any exceptions thrown inside an EventListener will not stop propagation
> > of the event. It will continue processing any additional EventListener
> > in the described manner.
> > /quote
>
> I just verified that IE6, Safari 2, Firefox 1.5 and Opera 9 really do
> this. When an error is thrown in one handler it seems like the other
> handlers still run.

Yeah, this is simple to fix in your code. Just put your handler
function call in a try/catch block (that does nothing with the error)
and you'll be fine. The only problem is when you /want/ your handler
to throw an error for some reason, though I imagine this will be the
case far less than wanting the other handlers to execute despite an
error.

>
> > I was concerned about your implementation's synchronous implementation;
> > however, it appears that the standard is not clear on whether multiple
> > events should be called synchronously or asynchronously. (I believe
> > the FF is asynchronous for multiple events, which would improve
> > performance considerably. Need test to determine whether this is the
> > case.)
>
> How could this be checked? I just tried a few big loops in one handler
> to keep it running and see if the other handler ran at the same time.
> That didn't give any good information.
>
> When would a handler really need to run for such a long period of time?
>

I was just thinking, not so much for handlers that run a really long
period of time. But say you have a click event that launches two
handlers for two different widgets, and each widget is animated
somehow. You want the animations to be synchronized with each other,
not one after the other. However, it appears after my test that the
browsers are single-threaded with regards to this, and a single
long-running event handler can hold up the others. (By the way, IE
executes handlers in reverse order of assignment, while Firefox
executes them in the order they were assigned. In case you were
wondering - not that this will change anything for you, since the W3C
spec leaves this to UAs to decide.)

Of course, a real animation would work using setTimeout or setInterval,
which allow you to simulate multi-threading. Since this is the case I
don't think synchronicity is something to worry about (since obviously
the UAs haven't).

One more thing I like about your approach is that you use

element.call(fn, e)

instead of a direct call to the function. This way the function's
"this" keyword will point to the element it's assigned to, which is
just how we always want it to be (don't know why the UAs -cough, cough
IE- can't figure this out).

-David

PS the test I used:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="text/javascript" src="baselib.js"></script>
<script type="text/javascript">
Events.add(window, 'load', function() {
Events.add($('twohandlers'), 'click', function() {
alert('done2');
});
Events.add($('twohandlers'), 'click', function() {
for (var i=0; i<9999999; i++);
alert('done1');
});
});

Events.add(window, 'unload', function() {
alert('Before clear:\n' + JsonRpc.serialize(Events.register));
Events.clear();
alert('After clear:\n' + JsonRpc.serialize(Events.register));
});
</script>
</head>
<body>
<input type="button" id="twohandlers" value="two handlers test" />
</div>
</body>
</html>

You can get the relevant definitions of Events and Events.add from
earlier in the thread, but basically they're a wrapper for the DOM 2
event assigners, so you should be able to substitute your own.

Peter Michaux

unread,
Nov 30, 2006, 8:28:36 PM11/30/06
to
Peter Michaux wrote:

<snip>

> The Yahoo! UI library focuses on using addEventListener and
> attachEvent. However, due to the click and dblclick bugs in Safari a
> long legacy event workaround is included to use a Netscape4-type event
> model for Safari. Something like this
>
> var listeners = [function(event){}, function(event){}];
> document.getElementById('blue').onmouseover = function(event) {
> for (var i=0; i<listeners.length; i++) {
> listeners[i](event);
> }
> };

<snip>

> With all the new browsers I tested with this legacy workaround, the
> listener handlers can use event.stopPropogation() or
> event.cancelBubble=true and they work as desired. The handler functions
> can also use event.preventDefault() and event.returnValue=false and
> they too work. These seem to work because the event object passed to
> the handlers is a modern event object and not one from Netscape4.
>
> My question is, if Safari needs this legacy workaround, and the legacy
> workaround seems to work in all the browsers that have addEventListener
> or attachEvent, then why bother with the addEventListener and
> attachEvent functions at all? Why not just use the legacy way for all
> browsers and all type of events.?

For the record, I thought of an advantage to using addEventListener and
attachEvent.

If an element is served with an inline event handler like this

<div onmouseover="alert('hi');">

then using the legacy event workaround will clobber this inline
handler.

I've decided to have both the legacy workaround and the DOM2/IE types
like Yahoo! UI. I will use the legacy workaround for click and dblclick
events like Yahoo! UI does but I will do this for all browsers so they
are treated the same. For other event types I will use DOM2/IE.

Peter

David Golightly

unread,
Nov 30, 2006, 10:13:51 PM11/30/06
to

Peter Michaux wrote:
> For the record, I thought of an advantage to using addEventListener and
> attachEvent.
>
> If an element is served with an inline event handler like this
>
> <div onmouseover="alert('hi');">
>
> then using the legacy event workaround will clobber this inline
> handler.

Couldn't you just use code like

if (div.onmouseover) {
newlisteners.push(div.onmouseover);
}

to add the inline event to the builtin handlers before wiping it?

0 new messages