Skip to content

Instantly share code, notes, and snippets.

@jpiccari
Last active August 29, 2015 14:01
Show Gist options
  • Save jpiccari/c29b33eb757effee76aa to your computer and use it in GitHub Desktop.
Save jpiccari/c29b33eb757effee76aa to your computer and use it in GitHub Desktop.
AMD EventEmitter module that supports event filters (a.k.a namespaced events). Extends object passed to constructor. There is also a global emitter accessible via EventEmitter.global. (565 bytes minified, gzipped)
define(
'EventEmitter',
function() {
'use strict';
/**
* Parse event types into topic and filters.
*/
function parseEvent(event) {
event = event.split('.');
return {
topic: event.shift(),
/*
* Sorting the list of filters is pretty fast (O(n log n)) and
* allows us to use a simple and fast algorithim for matching
* up two sets of filters later.
*/
filters: event.sort()
};
}
/**
* Checks if all filters listed in target also exist in test.
*/
function matchFilters(target, test) {
var i,
j;
if (target.length <= test.length) {
// This for-loop is made possible by pre-sorting the filter arrays above
for (i = test.indexOf(target[0]), j = 0; i < test.length && j < target.length; i++) {
/*
* If the test string is less than the target string, this
* merely means we haven't started to any match yet and we
* we should increment the test index (i) but not the target
* index (j). We have no condition for this case, since the
* index for test will be incremented no matter what.
*
* If test and target strings are equal we can continue our
* match and increment the index for target (j), likewise the
* test index (i) will be implicitly incremeted at the end of
* the loop.
*/
if (test[i] === target[j]) {
j++;
}
/*
* If the test string is greater (alphabetically speaking)
* than the target string, it means we are no longer matching
* successfully and we should break.
*/
if(test[i] > target[j]) {
break;
}
}
// This is true only if all target filters were satisfied
return j === target.length;
}
return false;
}
/**
* EventEmitter method for setting an event.
*/
function on(event, fn, one) {
if (typeof fn === 'function') {
event = parseEvent(event);
if (!this._topics[event.topic]) {
this._topics[event.topic] = [];
}
this._topics[event.topic].push({
filters: event.filters,
fn: fn,
one: one
});
}
return this;
}
/**
* EventEmitter method for setting an event which only runs once.
*/
function one(event, fn) {
// Simply call EventEmitter.on() with the same options and 'one' set to true
return this.on(event, fn, true);
}
/**
* EventEmitter method for removing an event.
*/
function off(event) {
event = parseEvent(event);
var topic = this._topics[event.topic],
i;
if (!event.filters.length) {
topic.length = 0;
} else {
for (i = 0; i < topic.length; i++) {
// If the filters match, splice out the event object.
if (matchFilters(event.filters, topic[i].filters)) {
topic.splice(i--, 1);
}
}
}
return this;
}
/**
* EventEmitter method for triggering an event.
*/
function trigger(event) {
var triggerCall,
topics,
i,
j;
// Enqueue the trigger call
trigger._queue.push({ obj: this, args: Array.apply(0, arguments) });
// If we aren't currently flushing the trigger queue, then flush it
if (!trigger.isFlushing) {
trigger.isFlushing = true;
for (i = 0; i < trigger._queue.length; i++) {
triggerCall = trigger._queue[i];
event = parseEvent(triggerCall.args.shift());
topics = triggerCall.obj._topics[event.topic];
for (j = 0; topics && j < topics.length; j++) {
if (matchFilters(event.filters, topics[j].filters)) {
topics[j].fn.apply(triggerCall.obj, triggerCall.args);
// If the 'one' property was set, splice out the event object.
if (topics[j].one) {
topics.splice(j--, 1);
}
}
}
}
trigger.isFlushing = false;
}
return this;
}
trigger._queue = [];
function EventEmitter(obj) {
// Test for objects while also weeding out null
if (obj !== Object(obj)) {
throw new TypeError(Object.prototype.toString.call(obj) + ' is not an object.');
}
// Don't overwrite obj._topics if is already an EventEmitter
obj._topics = obj._topics || {};
/*
* Setting the rest of the properties to reference the functions above
* achieves a similar effect to a prototype, without the possiblity
* of overwriting an object's current prototype. This gives us the
* most flexibilty and efficient memory usage.
*/
obj.on = on;
obj.one = one;
obj.off = off;
obj.trigger = trigger;
return obj;
}
// Global EventEmitter for more of a PubSub flavor
EventEmitter.global = EventEmitter({});
// Expose the interface
return EventEmitter;
}
);
define("EventEmitter",function(){function g(a){a=a.split(".");return{a:a.shift(),filters:a.sort()}}function k(a,b){var c,d;if(a.length<=b.length){c=b.indexOf(a[0]);for(d=0;c<b.length&&d<a.length&&!(b[c]===a[d]&&d++,b[c]>a[d]);c++);return d===a.length}return!1}function l(a,b,c){"function"===typeof b&&(a=g(a),this._topics[a.a]||(this._topics[a.a]=[]),this._topics[a.a].push({filters:a.filters,f:b,g:c}));return this}function m(a,b){return this.on(a,b,!0)}function n(a){a=g(a);var b=this._topics[a.a],c;
if(a.filters.length)for(c=0;c<b.length;c++)k(a.filters,b[c].filters)&&b.splice(c--,1);else b.length=0;return this}function e(a){var b,c,d,f;e.b.push({e:this,c:Array.apply(0,arguments)});if(!e.d){e.d=!0;for(d=0;d<e.b.length;d++)for(b=e.b[d],a=g(b.c.shift()),c=b.e._topics[a.a],f=0;f<c.length;f++)k(a.filters,c[f].filters)&&(c[f].f.apply(b.e,b.c),c[f].g&&c.splice(f--,1));e.d=!1}return this}function h(a){if(a!==Object(a))throw new TypeError({}.toString.call(a)+" is not an object.");a._topics=a._topics||
{};a.on=l;a.one=m;a.off=n;a.trigger=e;return a}e.b=[];h.global=h({});return h});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment