Event Notification System Friday, 4 January 2008

An Event Notification System is an object that manages notification of events to multiple callbacks. The Event Notification System uses an Event Registry to store the callbacks as bound methods. When the event fires, the callbacks are invoked.

Event Registry

An Event Registry is a store of bound methods. An Event Registry is used by an Event Notification System. The Event Notification System is tightly coupled with the Event Registry. Sometimes it is referred to as the Registry. In reality, the Registry is just a data structure and the Event Notification System is a behavioral object.

Almost Every JavaScript library has an Event Registry, or at least some way of dealing with event notification.

For example:

// YUI:
YAHOO.util.Event.addListener( link, "click", linkClickHandler, thisArg );

// Prototype: (not a registry, but the old 'addEvent' function renamed).
Event.observe( link, "click", linkClickHandler ); 

// Dojo:
dojo.connect( link, "onclick", window, "linkClickHandler" );

They're all different in how they work.

The Event Registry is useful for a few reasons.

  • It allows multiple callbacks to be assigned to a function call.
  • Provides a usable alternative to attachEvent. Internet Explorer 7 and below has attachEvent/detachEvent. The callback function for attachEvent executes in global context (this is window), not the object it was attached to.

A good Event Registry solves these problems. A good Event Registry also allows for context resolution with an optional thisArg. A good Event Registry also allows custom events to be registered using the same interface.

A poorly designed Event Registry concerns itself with things related to native events (DOMContentLoaded, keyPress, et c). A poorly designed Event Registry does not pass an event object to the callback (perhaps trying to use eval to pass varargs).

Error Handling in an Event Notification System

Callback Errors Should not Break the Registry

A good Event Registry does not allow any callback to break the registry.

One common problem in most Event Notification Systems (such as Dojo, Mochikit, YUI, and jQuery) is that they allow the callback to break the System. If a callback fails, it prevents subsequent callbacks from firing. A callback should not be given the ability to break the Registry.

Here's how to break a Registry that doesn't consider errors:

var passed = false;
addCallback( link, "click", function(){ setTimeout(checkTitle, 500); } );
addCallback( link, "click", function(){ throw Error('bad'); } );
addCallback( link, "click", function(){ passed = true; } );

function checkTitle(){ 
    if(!passed) 
        alert("registry broken: second callback did not fire.");
    else 
        alert('passed');
}

Callbacks sometimes throw Errors. It is important for the Event Registry to consider this and take the responsibility to handle these errors properly. If an error occurs in a callback, it should not break the Registry.

It should be guaranteed that all callbacks fire, even when earlier callbacks throw errors. This is a natural expectation; it's exactly how DOM Events work:

DOM Events Test

(function(){
var s = document.getElementById('r-test');

var el = document.getElementById("registry-dom-event-button");
if(!window.hasDocumentListeners) {
    el.addEventListener( "click", setUpCheck, false );
    el.addEventListener("click", throwError, false );
    // setTitle must fire.
    el.addEventListener( "click", setTitle, false ); 

    window.hasDocumentListeners = true;
}
function setUpCheck(){ setTimeout(checkTitle, 500); }
function throwError(){ document.title = ""; throw Error('bad'); }
function setTitle(){ document.title += 'ok'; }
function checkTitle(ev) {
    if(document.title != "ok") {
        alert("DOM Events broken: setTitle did not fire. " + document.title );
    }
    else {
        alert("passed");
    }
}
})();

Result and Analysis

There should be 1 error and an alert passed. This indicates that after the error happened, the setTitle callback successfully fired.

This example assumes:

  • Callbacks fire in the order in which they were registered.
  • The bad error in the first callback does not stop subsequent callbacks from firing.
  • The button supports the EventTarget interface (not in IE7).
  • The button supports onfocus (not in safari bug 16331).

Proper Callback Error-Handling

Throwing the error in a separate thread allows the callstack to continue without breaking. Any errors that are thrown are thrown in the correct order in the callstack. The Event Publisher's fire function would have something like this:

try {
// If an error occurs, continue the event fire,
// but still throw the error.
  callback.call( thisArg, ev );
}
catch( ex ) {
  setTimeout("throw ex;", 1); 
}

The one subtle issue is that setTimeout uses global scope, like the Function constructor, not like eval, which runs in the calling context's scope.

A closure must be used to preserve the ex variable.

try {
// If an error occurs, continue the event fire,
// but still throw the error.
  callback.call( thisArg, ev );
}
catch( ex ) {
  setTimeout(function(){ throw ex; }, 1); 
}

Event Registry Test

The remaining problem with the above code is that the error condition is untestable. Writing a test suite forced me to realize this and I changed the design.

try {
if(csi[0].call(csi[1], e) == false)
  preventDefault = true; // continue main callstack and return false afterwards.
}
catch(ex) {
  APE.deferError(ex);
}

Where APE.deferError is defined:

deferError : function(error) {		
  setTimeout(function deferError(){throw error;},1);
}

I have included the source code for my own Event Registry, along with this test, which shows how I managed to test APE.deferError.

Performance?

Wrapping each callback call in a try catch might seem to be bad for performance. I tried it with mousemove event on my drag code, dragging multiple drag objects at a time (example), and it seemed fast enough; I did not notice performance problems in any browser. There is most likely some performance overhead using this approach, but I did not find a need to write a benchmark.

src should never be a string. Although this may seem obvious, YUI actually allows src to be a string, where the string represents an element's ID. The document is polled regulary until the element with the id matching string is found and then the callback is attached to that element. If the element has been renamed, the document is still polled and silent failure occurs.

This can lead to silent failure or corrupted application state if the element is not found. It is not recommended.

Packaging and API Design

The Event Notification System is a low level component with no external dependencies.

Being a low level component, the Event Notification System should be maximally stable (no efferent couplings), and maximally abstract. In this case, the Event Notification system is maximally abstract because it can't be subclassed or used independently.

Stable Dependencies Principle

Depend in the direction of stability

Stable Abstractions Principle

A package should be as abstract as it is stable.

Reuse Equivalence Principle

The Granule of Reuse is the Granule of Release.

The Event Notification System is a low level component with no external dependencies. It is intentionally packaged as a single, tested unit. It amplifies the essential (event notification) and eliminates the irrelevant.

Creating special cases for handling DOM events (keyCode, et c), would reduce abstraction. These special cases are perfectly valid, but do not belong in the Registry. Special case needs can either be hard-coded into end-implementation code (using feature/capability detection) or, if the special-case logic is complex, programmed into an object that performs a task (such as an Adapter object).

An example of an Adapter object would be a Content Load Adapter or a KeyEvent Adapter (key events are highly inconsistent across platforms). Such objects would be slightly higher-level and, having at least one dependency, would be less stable (though this is not a bad thing).

Department Store JavaScript

[insert_popular_library_name_here] usually include more code than any one application could possibly use in an attempt to cover the needs of every application.

Libraries that add more functionality into one module than is usually needed, or create modules that are not cohesive do so in spite of commonly known software package design concepts. The one-stop library approach is appealing because it allows developers to "stop cobbling bits of javascript."

Performance (Again)

Load Time Performance problems can be acheived by creating custom javascript builds on the server. Hand-rolled "combination" files or utils files are fine for web sites with fewer pages. Sites that don't require 200k+ of additional javascript should not include such functionality.

Technorati Tags:

Posted by default at 9:42 PM in Uncategorized

 

 

Comment: kangax at Mon, 31 Mar 9:48 PM
Great post Garrett.

I just fail to see why throwing error needs to be deferred. Could you please explain?

Best,
kangax
Comment: Garrett at Sat, 5 Apr 12:45 AM
Throwing an error in a setTimeout ensures that the callbacks continue firing without breaking the registry.

Without that, an error in a callback stops all other callbacks from firing. It makes the registry extremely fragile. Unfortunately, almost every event registry works this way.
Comment: Garrett at Sat, 22 Jan 11:46 AM
For example, in jQuery, when an event callback throws an error, the system breaks, preventing callbacks from firing.

jQuery(document).bind("click",
// This function fires.
function(){
alert(2);
throw new Error("blah");
}
);
jQuery(document).bind("click",
// This function shold fire, but does not.
function(){
alert(2);
throw new Error("blah");
}
);

With jQuery, the second callback won't fire. In contrast, with <code>document.addEventListener</code>, all callbacks will fire.

See also:
http://forum.jquery.com/topic/bug-the-left-event-handlers-will-be-ignored-after-error-was-thrown

Your comment:

(not displayed)
 
 

 

*AnimTree
*Tabs
*GlideMenus
*DragLib