Introduction
This article will cover the event framework. There will be an explanation of the framework and an implementation example.
Background
When producing a drag and drop editor, I needed an event framework that would allow me to register multiple delegates with an event (excuse the .NET terminology). The following is the result of that requirement.
This framework also includes code for implementing timers. I'll explain why this was required in the "Points of Interest" section.
The Code
The following is the code for the event framework.
An Easy Life with Prototyping
Collections in most frameworks include methods for adding and removing items from the collection. JavaScript doesn't by default, but they can be added by using prototyping. The first section of this framework prototypes on top of Array
to extend it with the Add
, RemoveAt
, and IndexOf
functions.
Array.prototype.Add = function(obj) { this[this.length] = obj; }
Array.prototype.RemoveAt = function(index) {
var result = new Array();
var offset = 0;
for (var x = 0; x < this.length; x++) {
if (x != index)
result[result.length] = this[x];
}
return result;
}
Array.prototype.IndexOf = function(value) { for (var x = 0; x <
this.length; x++) if (this[x] == value) return x; return -1; }
var uiEventRegistration = function() {
this.ID = null;
this.Method = null;
this.Caller = null;
}
The following code creates a framework for using timers. There is a timer array for holding all the created timers. A function called "uiTimerExecute
" is used to check the timer expired states. There is also the uiTimer
class which recieves an interval (in milliseconds) in the constructor.
var uiTimers = new Array();
var uiTimerCount = 0;
function uiTimerExecute(timerID) {
for(var x = 0; x < uiTimers.length; x++) {
if (timerID == null || uiTimers[x].TimerID == timerID) {
if (uiTimers[x].Expired()) {
uiTimers[x].OnTick.Fire();
var d = new Date();
uiTimers[x].NextTime = new Date(d.getYear(),d.getMonth(),d.getDate(),
d.getHours(),d.getMinutes(),d.getSeconds(),
d.getMilliseconds() + (uiTimers[x].Interval));
}
}
}
}
var uiTimer = function(interval) {
uiTimers.Add(this);
this.Interval = interval;
this.StartDate = new Date();
this.NextTime = new Date(this.StartDate.getYear(),this.StartDate.getMonth(),
this.StartDate.getDate(),this.StartDate.getHours(),
this.StartDate.getMinutes(),
this.StartDate.getSeconds(),
this.StartDate.getMilliseconds() + (interval));
this.OnTick = new uiEvent();
this.OnTick.TimerEvent = true;
this.Expired = function() { return this.NextTime < new Date(); };
this.TimerID = uiTimerCount;
setInterval('uiTimerExecute(' + this.TimerID + ');', interval);
}
The last section is the main section of the event framework. There is a class called uiEvent
. This has a method "Register
". This is passed a delegate and a context object. The context object is the object referred to as "this
" when the delegate is called by the method. Register
returns a registration ID. This can be used with the "Deregister
" method to remove the delegate from the event stack. The last method is "Fire
". When this is called, all parameters passed in are then sent as parameters to the registered delegates.
var uiEvent = function() {
this.TimerEvent = false;
this.__registered = new Array();
this.__currentID = 0;
this.Register = function(func, caller) {
var reg = new uiEventRegistration();
reg.ID = this.__currentID;
reg.Method = func;
reg.Caller = caller;
this.__registered.Add(reg);
var returnID = this.__currentID;
this.__currentID++;
return returnID;
}
this.Deregister = function(RegistrationID) {
var index = -1;
for (var x = 0; x < this.__registered.length; x++)
if (this.__registered[x].ID == RegistrationID) {
index = x;
break;
}
if (index != -1) {
this.__registered = this.__registered.RemoveAt(index);
}
}
this.Fire = function() {
if (!this.TimerEvent)
uiTimerExecute();
var a = arguments;
for (var x = 0; x < this.__registered.length; x++) {
this.__registered[x].Method.call(this.__registered[x].Caller,
a[0] == null ? null : a[0],
a[1] == null ? null : a[1],
a[2] == null ? null : a[2],
a[3] == null ? null : a[3],
a[4] == null ? null : a[4],
a[5] == null ? null : a[5],
a[6] == null ? null : a[6],
a[7] == null ? null : a[7],
a[8] == null ? null : a[8],
a[9] == null ? null : a[9])
}
}
}
For the Fire
event, there were two possible approaches. When a function is passed as a parameter, you can execute the "call
" method on the function. This has the same effect as invoking through Reflection. The second option was to use eval
to call the method. eval
is considerably slower than using the "call
" method. When using the "call
" method, there is a limitation. The parameters for call
need to be explicitly included in the call. This means there is a fixed limit to the number of parameters an event can handle. The good news is the framework can be extended to handle as many as required. The call above includes parameters such as: a[0] == null ? null : a[0]
.
By continuing past index position 9, you can extend the parameter count limitation as far as required for your implementation.
Using the Code
The following is one of the most basic implementations of the framework. Try the following code:
var eventTest = new uiEvent();
var context1 = { Name: 'Dave' };
var context2 = { Name: 'Chris' };
function delegate1(mood) {
alert(this.Name + ' is ' + mood);
}
function delegate2(mood) {
alert(mood + ' is not a good thing for ' + this.Name);
}
eventTest.Register(delegate1, context1);
eventTest.Register(delegate2, context2);
eventTest.Fire('sad');
The following message boxes will be displayed:
- "Dave is sad"
- "sad is not a good thing for Chris"
Points of Interest
There are base events within the DOM such as onmousemove
, onkeypress
. You would assume that with a single thread, all code stacks would be assigned the same priority and processed in order. This is not the case, e.g., if you have a mouse move event which is quite an intensive process. If a subsequent mouse move event fires before the previous finishes (no idle time on the thread), the mouse move event will always be given priority over executions planned using setInterval
and setTimeout
. This became an issue when implementing the Animation Framework, that will be described in a later document.
Therefore in this framework, I have created a timer event. When a non-timer event fires, it checks all the registered timer events to see if they have expired, and if so, they get executed first. The following is an example implementation of the timer event.
This example implements a second timer which increments a count every second and updates the window status.
var secondTicks = 0;
function secondIncrement() {
secondTicks++;
window.status = secondTicks;
}
var secondTimer = new uiTimer(1000);
secondTimer.OnTick.Register(secondIncrement);