JavaScript/Notes/EventNotificationSystem

From Noisebridge
Jump to navigation Jump to search

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.

An event is a function call that is fired after something has occurred.

Some examples of events generated by the web browser are "element.onclick" and "navigator.ononLine".

Event Registry[edit]

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:[edit]

<source lang="javascript">// 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" ); </source>

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[edit]

Callback Errors Should not Break the Registry[edit]

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:

<source lang="javascript">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: last callback did not fire.");
   else 
       alert('passed');

} </source>

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[edit]

<source lang="javascript">(function(){

var passed = false;

var el = document.getElementById("registry-dom-event-button"); el.addEventListener( "click", setUpCheck, false ); el.addEventListener("click", throwError, false );

 // setTitle must fire.

el.addEventListener( "click", setTitle, false );

function setUpCheck(){ setTimeout(checkTitle, 500); } function throwError(){ throw Error('bad'); } function setTitle(){ passed = true; } function checkTitle(ev) {

   if(!passed) {
       alert("DOM Events broken: setTitle did not fire. ");
   } else {
       alert("passed");
   }

} })();</source> jsbin


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:

Proper Callback Error-Handling[edit]

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:

<source lang="javascript">try { // If an error occurs, continue the event fire, // but still throw the error.

 callback.call( thisArg, ev );

} catch( ex ) {

 setTimeout("throw ex;", 1); 

} </source>

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.

<source lang="javascript">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); 

} </source>


Event Registry Test[edit]

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.

<source lang="javascript">try { if(csi[0].call(csi[1], e) == false)

 preventDefault = true; // continue main callstack and return false afterwards.

} catch(ex) {

 APE.deferError(ex);

} </source>

Where APE.deferError is defined:

<source lang="javascript">deferError : function(error) {

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

} </source>

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?[edit]

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[edit]

Depend in the direction of stability

Stable Abstractions Principle[edit]

A package should be as abstract as it is stable.

Reuse Equivalence Principle[edit]

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 <acronym title="Document Object Model">DOM</acronym> 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 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[edit]

[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)[edit]

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.