I've been using AddEvent for a couple of years now - the version that won that contest a while ago - and I'm very impressed with it. I recently checked out the site again and saw there was a new version, and I wanted to query one of the changes in it.
Specifically, in the first version of addEvent you constructed an event-handler wrapper function using the "function(e) { ...body of function... }" syntax. In the new version, you construct it using "new Function('e', '...body of function...')". What was the reason for the switch?
According to my reading (http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objec ts:Function#General), a function created using the Function() constructor is slower (at least in Gecko) because the function has to be interpreted every time it is called, whereas a function defined by "function() {}" is compiled at parse time.
Also, since t is the only variable external to this newly-created function, couldn't memory be saved by defining a function outside of addEvent which takes t as a second parameter, rather than defining this function as a closure every time that addEvent is called?
2) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Nevermind about the external function vs. closure, I expanded addEvent now and obviously it needs a closure. But still, why define the closure using new Function() instead of function() {}?
3) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Yet another question, what's the rationale for duplicating the addEvent arguments with rO = o, rT = t, rF = f, rL = l for the sake of the unload handler? Is this a way of avoiding perpetuating the memory leak?
4) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
I'm sorry for all the consecutive posts in this thread, next time I'll do all my experimenting beforehand!
Having done memory tests in IE versions 5.01 to 7, I now understand the choice to use the Function() constructor: this avoids creating a closure (and thus a memory leak) altogether. However it also appears to eliminate the need for any cleanup using the unload handler: removing the current unload code from the addEvent function does not result in memory leaks, allowing the addEvent function to be more compact. I'd also still suggest moving the bulk of the Function() body to an external function instead, which should theoretically speed up evaluation.
One concern is the use of a "for...in" loop to iterate the event handlers: in its current state it won't play well with any additions to the Object prototype. For this reason I suggest adding the check "if (!a[i]._i) continue;" to the loop, as this will skip any members that are not actual event handlers.
I've copy-pasted a modified version of the script below for review:
var addEvent = function(o, t, f, l) { var d = 'addEventListener', n = 'on' + t; if (o[d] && !l) return o[d](t, f, false); if (!o._evts) o._evts = {}; if (!o._evts[t]) { o._evts[t] = o[n] ? { b: o[n] } : {}; o[n] = new Function('e', 'return addEvent._c(e, this, this._evts["' + t + '"])');} } if (!f._i) f._i = addEvent._i++; o._evts[t][f._i] = f; } addEvent._i = 1; addEvent._c = function(e, o, a) { var r = true, i; for (i in a) { if (!a[i]._i) continue; o._f = a[i]; r = o._f(e||window.event) != false && r; o._f = null} return r; }
5) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Could I get some independent confirmation that the pruned version of the function above does not introduce memory leaks?
6) Angus TurnbullGroup: Moderators Posts: 4042Joined: 7 Dec 2003Location: New ZealandIP: 125.236.--.--
Posted:
Woah, sorry I missed your post initially!
Yeah, you are quite right on the Function() issue, I'm using it as it doesn't create a closure and is one less point of failure re: memory leaks. I don't see the advantage of putting it in addEvent._c if you're still using a new Function() anyway to call it -- since I've tried to optimise this script for small byte size you're not getting a speed advantage there at all or anything.
The variables are duplicated as I thought at the time that argument scope to a function wasn't inherited within a closure (like the onunload closure). That might be incorrect (I think it actually applies to XMLHttpRequest which is where I got the idea). That could slim the script down a bit, as a quick test reveals that both Ie and Firefox pass the argument scope of a function to its closure quite well.
Coders who add to Object.prototype are diabolically evil and should be punched in the face. People who add to Array.prototype are shifty and should be watched lest they graduate to messing with Object.prototype ;). Seriously, most of my scripts are compatible with Array.prototype modifications now (I made a bunch of workarounds) but "for..in" is a very legitimate JS method and IMHO coders should not have to work around other broken scripts that mess it up. I'll probably have to fix it regardless. Some browsers have a useful ".hasOwnProperty()" method on objects that comes in handy here.
The unload is still used as the functions for an event are stored as expando properties on the object itself in the DOM and I'm not comfortable leaving them there -- in a test with the FSMenu script (fairly event-intensive) I found not cleaning up onunload led to memory leaks.
Anyway it's midnight here and I'm too tired to test the script :(. Glad to see that you have been hacking on the script though and clearly have a good grasp of the code. addEvent functions are damn tricky to do well *and* concisely!
Cheers - Angus.
7) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Hi there! I shifted the bulk of the Function() code to a regular function declaration because (at least) Gecko parses functions created with Function() every time they are called. I figure the less code it has to parse each time, the faster it runs; this caveat may not apply to IE anyway though, and would need rigorous timing tests to verify that it makes any difference.
As far as the variable duplication goes - these look to be extraneous, and probably increase memory usage since a closure stores all variables from the scope it was created in.
You mention you retained the unload handler because there were still leaks on large pages due to leftover properties. However, in the original function the unload cleanup is only applied to the first event handler, not to any further ones - so these were still being left behind when the page unloads (as was the o._evt container).
I put together a reduced testcase for memory leaks, which just added 500 unique closures as onclick handlers on the document. There were no perceptible memory leaks nor differences in memory behaviour between the original function and the one I posted above, with or without the unload handler in both cases.
I also tested with variants which had the Function() call replaced with a regular closure: these leaked obviously and consistently, with or without the unload handler. However if the unload handler was moved so that it applied to each event handler, not just the first one, then the leaks were much smaller.
I agree with you about extensions to the Object prototype - they make the otherwise useful (for...in) loop structure awkward and unreliable. However, I feel it's better to implement a sanity-check and curse the programmers who made it necessary, than to ignore it and allow unexpected behaviour that's difficult to debug (and which would require the programmer to implement the same solution anyway). Plus the check I added is smaller and probably faster than a hasOwnProperty() call.
8) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
As usual, insight only hits after you've posted something... I made a better testcase that created 500 elements, gave them each two click event handlers, and stored a reference to the created element in each handler's closure. This time, the presence of the unload handler made all the difference - without it, there were obvious memory leaks.
However the unload handler definitely needs to be moved to the end of addEvent, since any additional event handlers after the first will not be cleaned up otherwise (and thus cause their own leaks).
9) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Thus, the revised version:
var addEvent = function(o, t, f, l) { var d = 'addEventListener'; if (o[d] && !l) return o[d](t, f, false); if (!o._evts) o._evts = {}; if (!o._evts[t]) { var n = 'on' + t; o._evts[t] = o[n] ? { b: o[n] } : {}; o[n] = new Function('e', 'return addEvent._c(e||window.event, this, this._evts["' + t + '"])'); } if (!f._i) f._i = addEvent._i++; o._evts[t][f._i] = f; if (t != 'unload') addEvent(window, 'unload', function() {removeEvent(o, t, f, l)}); }
addEvent._i = 1; addEvent._c = function(e, o, a) { var r = true, i; for (i in a) { if (a[i]._i) {o._f = a[i]; r = o._f(e) != false && r; o._f = null;} } return r; }
10) Angus TurnbullGroup: Moderators Posts: 4042Joined: 7 Dec 2003Location: New ZealandIP: 125.236.--.--
Posted:
I'm glad your memory leak experiments matched mine. I found the same thing: that in simple script without the unload it didn't leak but as soon as you started storing closures etc. and events on DOM nodes leakage occurred without the onunload handler.
D'oh -- the unload event handler placement is an obvious thing I really should have caught myself :). In an earlier version I was simply wiping o._evts[t] entirely once per object, so it's hangover from that.
I'll add a check for _i to the next version (it's no great bloat really and will stop people complaining!) and also rearrange the unload (still kicking self here on that one). Will probably leave the Function() in place (if it's parsed, it's parsed regardless, and that's only once per event so no great loss), but have spotted another optimisation, we can set o._f=null outside the loop for a small speed boost :). Your modifications should be fine as-is!
Cheers - Angus.
11) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Good point about o._f - I'd suggest using delete on it instead, except that delete fails on certain cases in IE... I think because it cannot delete properties from the window object? (presumably because window is also the global object). Does this tally with your observations?
12) Angus TurnbullGroup: Moderators Posts: 4042Joined: 7 Dec 2003Location: New ZealandIP: 125.236.--.--
Posted:
I've used "delete" more for hashes over which you want to do a for..in loop, not really much on the DOM. If you're just worried about leaks setting the property to null is just as good.
- Angus.
13) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Not leaks, just leaving behind unused properties. But since one doesn't iterate the properties of a DOM node anyway (unless one likes trawling thousands of properties) then I suppose it makes no practical difference.
(huh, and I only just noticed that you're a fellow NZer!)
14) Alun BestorGroup: Guests IP: 84.250.--.--
Posted:
Ah oops, an oversight with that _i check: it will cause any original HTML event-handler attribute to get ignored, because that handler never gets the _i property added to it when it's put into the _evts() hash.
Unfortunately IE <= 5 doesn't support hasOwnProperty(), as this would be the most concise solution after all. Otherwise, the shortest fix I can think of is "if (o[n]) o[n]._i = -1".
15) Angus TurnbullGroup: Moderators Posts: 4042Joined: 7 Dec 2003Location: New ZealandIP: 202.137.--.--
Posted:
In fact why not just assign o[n] a regular incremented _i value, that way you can do:
var old = document.onclick; addEvent(document, 'click', stuff); // ...other code... removeEvent(document, 'click', old);
And hey, always good to meet another coder from Way Down Under :).