Skip to content

Instantly share code, notes, and snippets.

@rgrove
Created July 23, 2014 23:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rgrove/b619077c7a67016f89bb to your computer and use it in GitHub Desktop.
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.
"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