Built-in Event Listeners, JavaScript Objects, the DOM, and Memory Leaks
As I mentioned yesterday in my article about custom event listeners [1], I decided to tie built-in listeners into my Event object. I did this for two reasons. The secondary reason was simply because it was easier to deal with multiple methods with similar construct within the same object.
The primary reason was the result of using built-in listeners on DOM objects which called methods of JavaScript objects upon firing. In order to call these JavaScript object methods within the correct scope I had to bind the methods to the JS object which creates a new anonymous method with the same properties as the original, but scoped properly when called anonymously (like through a setTimeout or a user triggered event). When doing this with a relatively small number of DOM objects I didn’t have any major problems (I in fact didn’t know there was any problem at all). However, I started to see some major memory issues within IE and Firefox when I started adding and removing listeners to a large number of DOM objects, or creating and removing a large number of DOM objects to which I attached listeners.
After a lot of head scratching and reading I discovered that there is some miscommunication between the DOM garbage collector and the JavaScript garbage collector which run separately. Normally each garbage collector will clean up its own objects when it sees they aren’t being used anymore. However, when you create a connection between a DOM object and JS object that link needs to be explicitly broken to clean up properly. You’d think just running a removeEventListener/detachEvent would do the trick, and you’d be right – except when calling these built-in methods they require you provide the exact function call as a parameter as you used when creating the listener. When binding a method to its parent object using the bind method, you are actually creating a new JS object so when you create a listener using a bound method you need to use the same bound method to properly remove the listener. To do that you need to keep the bound method available to use when you’re ready to remove the listener.
I had to keep a copy of the bound method in memory which meant keeping an array of all created listeners so I could later remove them. Since I was already doing this with my custom listeners it only made sense do tie the built-in listeners into the same object to keep all my event related code together – especially since they were now doing very similar things.
getBuiltinListenerIdx
The getBuiltinListenerIdx method is very similar to the getActionIdx method in customer listener portion of the code yesterday. It performs the same function, but is simpler because the array structure for the built-in listeners is simpler. For built-in listeners I don't have to handle the firing of events because that's all taken care of for me by the inner workings of the DOM and JavaScript engines so I don't have to keep track of what to fire when or anything like that. The sole purposes of the array structure for built-in listeners it to hold a reference to the bound method call so a listener can be removed properly and to allow me to cleanup after the rest of the application if it doesn't remove listeners on its own (I'm not including that this time though). In order to locate the bound method in the array I also need to keep a reference to the object, event, action, and binding to make sure I'm getting the correct one.
addBuiltinListener
Adding a built-in listener is also pretty straight forward. If a binding exists then a bound method needs to be generated using action.bind(binding). After that it's just handling the various browser's built-in event listener registrations and storing a copy of the listener's components in the array.
removeBuiltinListener
Removing a built-in listener isn't quite as straight forward which is exactly the reason I've added built-in events into my custom event handler. The removeBuiltinListener isn't terribly complex, but to make it more useful I made it so you can provide as little or as much information as necessary. If you want to remove a specific listener then you'd provide the object, event, action, and binding as parameters which will then call detachListener (described below). Doing so will cause the function to look for a listener that matches those specific parameters and then break out of the loop (a listener matching the exact parameters shouldn't exist more than once if addListener functioned properly). However, if you provide less information then more searching is required and more listeners may get removed. If you just provide an object then all listeners registered to that object will be removed, which is perfect if you're planning on removing that object from the DOM.
detachListener
The detachListener method is where the built-in listener actually gets removed by using removeEventListener/detachEvent, but in order to be thorough I also make sure to delete all possible connections that could keep the JavaScript and DOM garbage collectors from cleaning up.
Conclusion
The addBuiltinListener and removeBuiltinListener methods can be used just like the addListener and removeListener methods described yesterday, except the objects they're attached to are DOM objects and the events are built-in events like mouseover, mouseout, load, etc. Like I said yesterday - its likely not the perfect solution, but its the one I found works for me and hopefully you can find some use in it.
Here's the code all together:
Example
Test Link [2]
Here are the source files:
Links:
[1] http://www.josh-davis.org/2007/04/10/custom-event-listeners
[2] http://www.josh-davis.org/2007/04/11/javascript-built-in-listeners-and-memory-leaks#testlink
[3] http://www.josh-davis.org/files/uploads/2007/05/built-in-listeners.html
[4] http://www.josh-davis.org/files/uploads/2007/05/built-in-listeners.js