Novemberborn, Straight lines circle sometime

Adding Events, Recoded

Peter-Paul Koch recently launched the adddEvent() recoding contest. Of course, I couldn’t say no to such a challenge, so here’s my article explaining my entry. You might want to write the short terminology overview I wrote a while back before diving into this article.

First off, let’s look at the requirements for the event code:

Then there are some niceties we might want:

With that in mind, I started writing. After three iterations of simplifying, I came to the current solution. Let’s look through it piece by piece.

Oh blimey, that looks like code!

var __DomEvents__ = new function()

I’m creating the necessary functions inside __DomEvents__. I’m using the underscores because the actual addEvent() and removeEvent() functions are exported into the window object and I want a nice little namespace of my own. Also note the new function() part: the methods for the script are defined in an anonymous class, which is immediately instantiated. JavaScript’s solution for Singletons.

var registry = {};

function registryKey(id, type, fn)
{
  return id + "#" + type + "#" + fn;
}

function isInRegistry(key)
{
  return registry[key] != null;
}

This is the code which keeps track of all event listeners. registry is a hash table in which the listeners are stored. registryKey() takes care of creating a unique key to store the listener with. It does this by combining the ID of the event target, the event type, and the string representation of the listener. Finally isInRegistry() is a utility method we can use to see if a key has already been registered.

function invoke(key, evt)
{
   if(!isInRegistry(key))
      return null;

   _evt = __DomEvents__.alterEventObject(evt);
   // If the alteration method returned an object, use that as the event
   // object.
   if(typeof _evt == "object")
      evt = _evt;

   var handler = registry[key];
   var scope = handler.scope;

   // Evade Function#apply()
   scope.__DomEvents_listener__ = handler.listener;
   scope.__DomEvents_listener__(evt);
   scope.__DomEvents_listener__ = null;
}

invoke() will make sure the listener is invoked in the right scope. First — of course — it makes sure the listener is still in the registry. It then calls alterEventObject(), which allows us to modify the event object before it’s sent to the listeners. And at last it’ll invoke the listener!

The next part is a bit tricky. We first load the handler, a small object stored in the registry which contains a reference to the listener and the scope. Then it sets a property, __DomEvents_listener__ (now if that isn’t unique, what else is???), which references the listener. This means that when the listener is invoked via the property its scope will be scope. That’s exactly what we want so we invoke the listener via the property and then set the property back to null. We ain’t leaving no traces, are we now?

var targetIdCount = 0;

function targetId(target)
{
   if(target == document)
      return "__DomEvents_ID_document";
   if(target == window)
      return "__DomEvents_ID_window";

   var id = target.getAttribute("id") || target.uniqueID;

   if(id == null){
      id = "__DomEvents_ID_" + targetIdCount++;
      target.setAttribute("id", id);
   }

   return id;
}

Remember how registryKey() uses the ID of the event target? We need a way to find this ID. For the document and window objects, we’ll use a static ID, for HTML elements we’ll try to derive the ID from the element. Our first attempt is the id attribute. If it doesn’t exist we check for uniqueID. The latter is a property in Internet Explorer which contains a unique id (you didn’t expect that, did you?) which you can use to reference the element (through document.getElementById()). If we still don’t have an ID, we’ll have to set it ourselves. IDs generated by this code are prefixed with __DomEvents_ID_. targetIdCount is used to make sure we assign unique IDs — with each assignment the count is updated. And finally we return the ID.

window.addEvent = function(target, type, listener, scope)
{
   var key = registryKey(targetId(target), type, listener);
   scope = scope || target;

   //    Check if the listener hasn't been registered already.
   if(isInRegistry(key))
      return false;

   // Hey, it's a new listener!
   handler = {
      listener:   listener,
      scope:      scope,
      invoker:    function(evt)
                  {
                     invoke(key, evt);
                  }
   }

   registry[key] = handler;

   if(target.addEventListener){
      target.addEventListener(type, handler.invoker, false);
   } else if(target.attachEvent){
      target.attachEvent("on" + type, handler.invoker);
   } else {
     return false;
   }

   // Reset variables:
   target = listener = scope = null;

   return true;
}

Yes! That’s the code you’ve been waiting for, right? Let’s go through this step by step. First we create the key for this listener. We also check if it’s already been registered. Since isInRegistry() takes a key as parameter, listeners are distinguished by their function, not the scope. Talking about which, the line scope = scope || target; means that the scope will be scope, unless it hasn’t been specified, in which case it’ll be target (and the this keyword refers to the event target the listener was added to, great!).

You can also see how the handler object is created. The invoker property holds the method which will be added as the DOM event listener — it’s just a wrapper for the listener you specify. The handler object is added to the registry and the invoker method is added as the event listener.

There are some key things in this code with regards to memory leaks. By storing the listener and scope in an object separate from the event target,
we can prevent circular references between the target and the listener and the target. And even though the invoker method is a closure, by setting the values of target, listener and scope to null we make sure IE won’t be taking a leak.

By now we are almost finished with the code. We only have to look at a way to remove the listeners:

window.removeEvent = function(target, type, listener, scope)
{
   var key = registryKey(targetId(target), type, listener);

   if(!isInRegistry(key))
      return false;

   var invoker = registry[key].invoker;

   registry[key] = null;

   if(target.removeEventListener){
      target.removeEventListener(type, invoker, false);
   } else if(target.detachEvent) {
      target.detachEvent("on" + type, invoker);
   } else {
      return false;
   }

   return true;
}

Actually I think you are perfectly capable of understanding this method. Onwards to the final cleaning up:

this.alterEventObject = function(){};

The alterEventObject() method is a dummy method for invoke(). Note how it’s set as __DomEvents__.alterEventObject().

Documentation for the public methods

addEvent()

Adds the listener.

Parameters:

removeEvent()

Removes the listener.

Parameters:

alterEventObject()

A function which can alter the event object. Override this to use your own.

Parameters:

And… that’s it!

Wow, I just explained the entire code! You can see the required example page and also the source (littered with custom-style documentation, that’ll have to be extracted in the near future). Like most of my other work the code is licensed under the CC-GNU LGPL.

Also I’d like to thank Michaël van Ouwerkerk for testing the script on Windows, and Lief van der Baan for checking the article for spelling and grammar mistakes. Thanks guys!

I hope you enjoyed the article. If you have any questions, you are welcome to contact me.

link | javascript | 11 September 2005, 04:30



Novemberborn: Extra

About the author

Mark Wubben is a hacker/writer in Enschede, the Netherlands.

Read more about Mark...

Go to

Jobs (NL)

Xopus zoekt programmeurs! Verbeter de code en win!

Subscribe