JavaScript/Notes/EventNotificationSystem: Difference between revisions

From Noisebridge
Jump to navigation Jump to search
Garrett (talk | contribs)
No edit summary
Garrett (talk | contribs)
No edit summary
Line 15: Line 15:
</p>
</p>


<h3>Event Registry</h3>
=== Event Registry ===
<p>
<p>
     An <dfn>Event Registry</dfn> is a store of bound methods.  
     An <dfn>Event Registry</dfn> is a store of bound methods.  
Line 27: Line 27:
</p>
</p>


<h4>For example:</h4>
==== For example: ====
<pre>// YUI:
<source lang="javascript">// YUI:
YAHOO.util.Event.addListener( link, "click", linkClickHandler, thisArg );
YAHOO.util.Event.addListener( link, "click", linkClickHandler, thisArg );


// Prototype: <span style="color: rgb(153, 0, 0);">(not a registry, but the old 'addEvent' function renamed)</span>.
// Prototype: (not a registry, but the old 'addEvent' function renamed) .
Event.observe( link, "click", linkClickHandler );  
Event.observe( link, "click", linkClickHandler );  


// Dojo:
// Dojo:
dojo.connect( link, "onclick", window, "linkClickHandler" );
dojo.connect( link, "onclick", window, "linkClickHandler" );
</pre>
</source>


<p>
<p>
Line 64: Line 64:
</p>
</p>


<h3>Error Handling in an <dfn>Event Notification System</dfn></h3>
=== Error Handling in an <dfn>Event Notification System</dfn>< ===


<h4>Callback Errors Should not Break the Registry</h4>
==== Callback Errors Should not Break the Registry ====
<p>A good <dfn>Event Registry</dfn> does not allow any callback to break the registry.
<p>A good <dfn>Event Registry</dfn> does not allow any callback to break the registry.
</p>
</p>
Line 132: Line 132:


</div>
</div>
<h4>Result and Analysis</h4>
<h4>Result and Analysis</h4>


Line 152: Line 153:
</ul>
</ul>


<h3>Proper Callback Error-Handling</h3>
=== Proper Callback Error-Handling ===
<p>
<p>
     Throwing the error in a separate thread allows the callstack to continue without breaking. Any errors that are thrown  
     Throwing the error in a separate thread allows the callstack to continue without breaking. Any errors that are thrown  
Line 159: Line 160:
</p>
</p>


<pre>try {
<source lang="javascript">try {
// If an error occurs, continue the event fire,
// If an error occurs, continue the event fire,
// but still throw the error.
// but still throw the error.
Line 167: Line 168:
   setTimeout("throw ex;", 1);  
   setTimeout("throw ex;", 1);  
}
}
</pre>
</source>


<p>
<p>
Line 177: Line 178:
</p>
</p>


<pre>try {
<source lang="javascript">try {
// If an error occurs, continue the event fire,
// If an error occurs, continue the event fire,
// but still throw the error.
// but still throw the error.
Line 185: Line 186:
   setTimeout(function(){ throw ex; }, 1);  
   setTimeout(function(){ throw ex; }, 1);  
}
}
</pre>
</source>




<h3>Event Registry Test</h3>
=== Event Registry Test ===
<p>
<p>
   The remaining problem with the above code is that the error condition is untestable. Writing a test suite forced me to realize this  
   The remaining problem with the above code is that the error condition is untestable. Writing a test suite forced me to realize this  
Line 194: Line 195:
</p>
</p>


<pre>try {
<source lang="javascript">try {
if(csi[0].call(csi[1], e) == false)
if(csi[0].call(csi[1], e) == false)
   preventDefault = true; // continue main callstack and return false afterwards.
   preventDefault = true; // continue main callstack and return false afterwards.
Line 201: Line 202:
   APE.deferError(ex);
   APE.deferError(ex);
}
}
</pre>
</source>


<p>
<p>
Line 208: Line 209:
</p>
</p>


<pre>deferError : function(error) {
<source lang="javascript">deferError : function(error) {
   setTimeout(function deferError(){throw error;},1);
   setTimeout(function deferError(){throw error;},1);
}
}
</pre>
</source>


<p>
<p>
Line 219: Line 220:
</p>
</p>


<h3>Performance?</h3>
=== Performance? ===


<p>
<p>
Line 251: Line 252:
</p>
</p>


<h3>Stable Dependencies Principle</h3>
=== Stable Dependencies Principle ===
<p>Depend in the direction of stability</p>
<p>Depend in the direction of stability</p>


<h3>Stable Abstractions Principle</h3>
=== Stable Abstractions Principle ===
<p>
<p>


Line 260: Line 261:
</p>
</p>


<h3>Reuse Equivalence Principle</h3>
=== Reuse Equivalence Principle ===
<p>
<p>
     The Granule of Reuse is the Granule of Release.
     The Granule of Reuse is the Granule of Release.
Line 285: Line 286:
</p>
</p>


<h3>Department Store JavaScript</h3>
=== Department Store JavaScript ===
<p>
<p>
     [insert_popular_library_name_here] usually include more code than any one application could possibly use in an attempt to  
     [insert_popular_library_name_here] usually include more code than any one application could possibly use in an attempt to  
Line 296: Line 297:
</p>
</p>


<h3>Performance (Again)</h3>
=== Performance (Again) ===
<p>
<p>


     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.
     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.
</p>
</p>
        </div>
        <div class="links">
                        <span class="commentslink"> Responses on &quot;Event Notification System&quot;">
            <span></span>Responses (3)
            </span>
        </div>
       
<div class="comment">
                <a name="response-1"></a>
                            <strong>Comment:</strong> <span class="author">kangax</span> at Mon, 31 Mar 9:48 PM
                <div class="post">Great post Garrett.
<br>
<br>I just fail to see why throwing error needs to be deferred. Could you please explain?
<br>
<br>Best,
<br>kangax</div>
                        </div>
                                    <div class="comment">
                <a name="response-2"></a>
                            <strong>Comment:</strong> <span class="author"><a href="http://dhtmlkitchen.com/">Garrett</a></span> at Sat, 5 Apr 12:45 AM
                <div class="post">Throwing an error in a setTimeout ensures that the callbacks continue firing without breaking the registry.
<br>
<br>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.</div>
                        </div>
                                    <div class="comment">
                <a name="response-3"></a>
                            <strong>Comment</a>:</strong> <span class="author">Garrett</span> at Sat, 22 Jan 11:46 AM
                <div class="post">For example, in jQuery, when an event callback throws an error, the system breaks, preventing callbacks from firing.
<br><pre>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");
  }
);
</pre>
<br>With jQuery, the second callback won't fire. In contrast, with &lt;code&gt;document.addEventListener&lt;/code&gt;, all callbacks will fire.
<br>
<br>See also:
<br>http://forum.jquery.com/topic/bug-the-left-event-handlers-will-be-ignored-after-error-was-thrown</div>

Revision as of 19:02, 18 December 2013

Event-Notification-System

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

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:

<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<

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:

<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

<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

<source lang="html4Strict">

<button id="registry-dom-event-button" onfocus="eval(document.getElementById('registry-dom-event-test').textContent)">eval</button> </source>

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

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

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 <a href="http://dhtmlkitchen.com/ape/test/tests/EventPublisher-test.html" onclick="window.open(this.href);return false;">test</a>, 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 (<a href="http://dhtmlkitchen.com/ape/example/drag/droptarget/">example</a>), 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 <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 a <a href="http://dhtmlkitchen.com/learn/js/load/">Content Load Adapter</a> 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.