Created
July 23, 2014 23:00
-
-
Save rgrove/b619077c7a67016f89bb to your computer and use it in GitHub Desktop.
Simple ES5 custom event implementation with basic bubbling support, for server or client.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
/** | |
Barebones custom events implementation. Extend or mix in this class to add event | |
support to your own classes. | |
Example: | |
function MyClass() { | |
// Example of attaching a listener (this isn't required). | |
this.on('somethingHappened', this.onSomethingHappened); | |
} | |
MyClass.prototype = Object.create(EventEmitter.prototype); | |
MyClass.prototype.constructor = MyClass; | |
MyClass.prototype.doSomething = function () { | |
this.emit('somethingHappened', {foo: 'bar'}, 'all args are arbitrary'); | |
}; | |
MyClass.prototype.onSomethingHappened = function (obj, string, e) { | |
// Called when the `somethingHappened` event is emitted, with whatever | |
// arguments are passed to `emit()`. The final argument will be an event | |
// object containing metadata about the event, such as its type, handle, | |
// and target. | |
}; | |
**/ | |
function EventEmitter() {} | |
/** | |
Adds the given EventEmitter instance as a bubble target of this EventEmitter. | |
When an event is emitted by this EventEmitter, it will first execute its own | |
listeners (if any), and will then re-emit the event on the bubble target. | |
@param {SM.EventEmitter} target | |
EventEmitter instance to add as a bubble target. | |
@chainable | |
**/ | |
EventEmitter.prototype.addTarget = function addTarget(target) { | |
if (this !== target) { | |
this._initEventEmitter(); | |
// TODO: [ES6] Use a WeakMap. | |
this._eventEmitter.targets.push(target); | |
} | |
return this; | |
}; | |
/** | |
Emits an event of the given _type_, executing any listeners that have been | |
registered for that event. | |
Any arguments passed to `emit()` beyond _type_ will be passed along to | |
listeners. | |
@param {String} type | |
Arbitrary event name to emit. | |
@param {Any} [...args] | |
Zero or more arguments to pass to listeners. | |
@chainable | |
**/ | |
EventEmitter.prototype.emit = function emit(type) { | |
var emitterData = this._eventEmitter; | |
// If no listeners or bubble targets have been attached, there's nothing to | |
// do. | |
if (!emitterData) { | |
return this; | |
} | |
// Create an aggregate list of handles from all targets, starting with this | |
// instance and including all bubble targets. | |
// | |
// Aggregation is breadth-first, starting with this emitter's handles, then | |
// each of its targets' handles, then each of _their_ targets' handles, and | |
// so on. Once visited a target will never be revisited, so loops are | |
// impossible. | |
var handles = [], | |
targets = [this], | |
targetsSeen = {}, | |
currentTarget, | |
targetEmitterData, | |
targetHandles; | |
for (var i = 0; i < targets.length; ++i) { // length may increase during iteration | |
currentTarget = targets[i]; | |
targetEmitterData = currentTarget._eventEmitter; | |
if (targetEmitterData && !targetsSeen[targetEmitterData.id]) { | |
targetHandles = targetEmitterData.events[type]; | |
if (targetHandles) { | |
handles.push.apply(handles, targetHandles); | |
} | |
if (targetEmitterData.targets.length) { | |
targets.push.apply(targets, targetEmitterData.targets); | |
} | |
targetsSeen[targetEmitterData.id] = true; | |
} | |
} | |
// No handles found on any targets? Nothing to do! | |
if (!handles.length) { | |
return this; | |
} | |
// Passing `arguments` to `Array.prototype.slice()` would deoptimize this | |
// function in v8, so we arrayify it here manually. | |
// | |
// This info is current as of Chrome 35.0.1916.153. | |
var argCount = arguments.length, | |
args = new Array(argCount); | |
for (i = 1; i < argCount; ++i) { | |
args[i - 1] = arguments[i]; | |
} | |
// Execute each listener in the order they were attached. | |
var handleCount = handles.length, | |
handle; | |
for (i = 0; i < handleCount; ++i) { | |
handle = handles[i]; | |
args[argCount - 1] = { | |
currentTarget: handle.currentTarget, | |
handle : handle, | |
target : this, | |
type : handle.type | |
}; | |
// Intentionally not catching exceptions in event handlers because: | |
// | |
// 1. That would deoptimize this function. | |
// | |
// 2. An unhandled exception is a bug, and event handlers should never | |
// cause unhandled exceptions. | |
handle.listener.apply(handle.thisObj, args); | |
} | |
return this; | |
}; | |
/** | |
Registers the given _listener_ to be called whenever an event of the specified | |
_type_ is emitted. | |
Note that to remove a specific event listener, you must keep a reference to the | |
event handle object returned by this method. | |
@param {String} type | |
Event type to listen for. | |
@param {Function} listener | |
Listener function to call when this event is emitted. The listener will | |
receive any arguments that are passed to `emit()`. | |
The last argument passed to the listener will always be an event data object | |
containing the following properties: | |
@property {SM.EventEmitter} currentTarget | |
The EventEmitter instance to which the listener was attached, and which | |
is emitting the current event. In the case of a bubbled event, this may | |
be a different EventEmitter instance than the one that originally | |
emitted the event (see `target`). | |
@property {Object} handle | |
An event handle object containing metadata about this listener. This | |
object can be passed to `removeListener()` to detach this listener. | |
@property {SM.EventEmitter} target | |
The EventEmitter instance from which the original event was emitted. In | |
the case of a bubbled event, this may differ from `currentTarget`. | |
@property {String} type | |
The name of the event that was emitted. | |
@param {Object} [thisObj=this] | |
Object to set `this` to when the listener is called. Defaults to this | |
EventEmitter instance. | |
@return {Object} | |
An event handle object containing metadata about this listener. This event | |
handle can be passed to `removeListener()` to detach this listener. | |
Event handle objects have the following properties: | |
@property {Function} listener | |
The listener function attached to this event. | |
@property {Object} thisObj | |
The `this` object that will be used when calling the listener function. | |
@property {String} type | |
The event type specified in the `on()` call. | |
**/ | |
EventEmitter.prototype.on = function on(type, listener, thisObj) { | |
this._initEventEmitter(); | |
var events = this._eventEmitter.events, | |
handle; | |
handle = { | |
currentTarget: this, | |
listener : listener, | |
thisObj : thisObj || this, | |
type : type | |
}; | |
events[type] || (events[type] = []); | |
events[type].push(handle); | |
return handle; | |
}; | |
/** | |
Removes all listeners from the given event _type_, or from all event types if no | |
_type_ is specified. | |
@param {String} [type] | |
Event type whose listeners should be removed. If not specified, all | |
listeners will be removed from all event types. | |
@chainable | |
**/ | |
EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { | |
var events = this._eventEmitter && this._eventEmitter.events; | |
if (events) { | |
if (type) { | |
if (events[type]) { | |
delete events[type]; | |
} | |
} else { | |
this._eventEmitter.events = {}; | |
} | |
} | |
return this; | |
}; | |
/** | |
Removes the listener with the given event _handle_. | |
@param {Object} handle | |
Event handle object whose listener should be removed. This object is | |
returned by `on()` when an event listener is attached. | |
@chainable | |
**/ | |
EventEmitter.prototype.removeListener = function removeListener(handle) { | |
var events = this._eventEmitter && this._eventEmitter.events, | |
handles = events && events[handle.type]; | |
if (handles) { | |
// Reverse loop, since more recently attached listeners are more likely | |
// to be removed. | |
for (var i = handles.length; --i > -1;) { | |
if (handles[i] === handle) { | |
handles.splice(i, 1); | |
break; | |
} | |
} | |
// If the last handle for this event type was removed, remove the | |
// handles array to short-circuit future emits until another listener is | |
// attached. | |
if (handles.length === 0) { | |
delete events[handle.type]; | |
} | |
// Nullify references in the handle in order to allow GC even if someone | |
// accidentally holds onto the handle object. | |
handle.listener = null; | |
handle.thisObj = null; | |
} | |
return this; | |
}; | |
/** | |
Removes the given EventEmitter instance as a bubble target of this EventEmitter. | |
@param {SM.EventEmitter} target | |
EventEmitter instance to remove as a bubble target. | |
@chainable | |
**/ | |
EventEmitter.prototype.removeTarget = function removeTarget(target) { | |
var targets = this._eventEmitter && this._eventEmitter.targets; | |
if (targets && targets.length) { | |
var index = targets.lastIndexOf(target); | |
if (index > -1) { | |
targets.splice(index, 1); | |
} | |
} | |
return this; | |
}; | |
// -- Protected Prototype Properties ------------------------------------------- | |
/** | |
Initializes this EventEmitter instance if it hasn't already been initialized. | |
@protected | |
**/ | |
EventEmitter.prototype._initEventEmitter = function _initEventEmitter() { | |
if (this._eventEmitter) { | |
return; | |
} | |
EventEmitter._guidCount || (EventEmitter._guidCount = 0); | |
Object.defineProperty(this, '_eventEmitter', { | |
value: { | |
events : {}, | |
id : '' + (EventEmitter._guidCount += 1) + Math.random(), | |
targets: [] | |
} | |
}); | |
}; | |
module.exports = EventEmitter; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment