Backbone vs Riot Observer vs EventEmitter (v6)

Revision 6 of this benchmark created by Joshua Piccari on


Preparation HTML

<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>

<script>
var splitEventString,
        Observer;

splitEventString = function(str) {
        return str.trim().split(/\s+/);
};

/* The Observer Constructor
 ================================================================================== */
Observer = function() {
        this._events = {};
};

/* The Observer's prototyped methods
 ================================================================================== */
Observer.prototype = {

        on: function(event, callback, context) {
                // split the events from a space-delimited string into an array
                var events = splitEventString(event),
                        i;
                
                /**
                 * loop through the events and push the callback/context into the
                 * instance's _events object's appropriate array
                 */
                for (i = 0; i < events.length; i++) {
                        event = events[i];
                        this._events[event] = this._events[event] || [];
                        this._events[event].push({
                                callback: callback,
                                context: context
                        });
                }

                return this;
        },

        off: function(event, callback, context) {
                // if there isn't an event or callback passed, remove all bound events
                if (!event && !callback) {
                        this._events = {};
                } else {
                        // split the events from a space-delimited string into an array
                        var events = splitEventString(event),
                                retain,
                                listeners,
                                i,
                                j;
                        
                        // loop through the events
                        for (i = 0; i < events.length; i++) {
                                event = events[i];
                                listeners = this._events[event];

                                // if an event has bound listeners
                                if (listeners) {

                                        /* create a new array of listeners to be retained */
                                        this._events[event] = retain = [];
                                        
                                        /* 
                                         * loop through the existing listeners and if a callback is passed in 
                                         * and the callbacks don't match or a context is passed in and that 
                                         * doesn't match, retain them
                                         */
                                        for (j = 0; j < listeners.length; j++) {
                                                if ((callback && listeners[j].callback !== callback) || (context && context !== listeners[j].context)) {
                                                        retain.push(listeners[j]);
                                                }
                                        }

                                        // if no events were retained, delete the array entirely
                                        if (!retain.length) {
                                                delete this._events[event];
                                        }
                                }
                        }
                }

                return this;
        },

        trigger: function(event) {
                var listeners = this._events[event],
                        listener,
                        args = Array.prototype.slice.call(arguments, 1),
                        i;
                
                        if (listeners) {
                                for (i = 0; (listener = listeners[i]); i++) {
                                        listener.callback.apply(listener.context || this, args);
                                }
                        }

                return this;
        },

        one: function(event, callback, context) {
                var self = this,
                        originalCallback = callback;

                // wrap up the callback, so that, once it is fired, it is unbound
                callback = function() {
                        self.off(event, callback, context);
                        originalCallback.apply(this, arguments);
                };

                return this.on.apply(this, [event, callback, context]);
        }

};


function Observable() {
        this.callbacks = this.callbacks || {};
}

Observable.prototype = {

        /**
         * Adds callback(s) for the given event(s).
         * @param  {String}   events Whitespace-delimited list of event names
         * @param  {Function} fn     Callback function
         * @return {Object}          Reference to this Observable
         */
        on: function(events, fn) {

                var self = this;

                if (typeof fn === 'function') {
                        events.replace(/[^\s]+/g, function(name, pos) {
                                (self.callbacks[name] = self.callbacks[name] || []).push(fn);
                        });
                }

                return this;

        },

        /**
         * Removes the given callback(s) for the given event(s). A wildcard value ("*")
         * removes all events and their associated callbacks from the callbacks object.
         * Specifying a fn parameter will remove that callback for the given event(s).
         * Not specifying a fn parameter will result in all callbacks being removed for
         * the given event(s).
         * @param  {String}   events Whitespace-delimited list of event names
         * @param  {Function} fn     (optional) Callback function
         * @return {Object}          Reference to this Observable
         */
        off: function(events, fn) {

                var i = 0,
                        arr,
                        callback,
                        self = this;

                if (events === '*') {

                        this.callbacks = {};

                } else if (fn) {

                        arr = this.callbacks[events];

                        for (i; (callback = arr && arr[i]); i++) {
                                if (callback === fn) {
                                        arr.splice(i, 1);
                                }
                        }

                } else if (typeof events === 'string') {

                        events.replace(/[^\s]+/g, function(name) {
                                self.callbacks[name] = [];
                        });

                }

                return this;

        },

        /**
         * Same as Observable#on but callback function is only triggered a single
         * time and is then removed from the callbacks object.
         * @param  {String}   name Event name
         * @param  {Function} fn   Callback function
         * @return {Object}        Reference to this Observable
         */
        one: function(name, fn) {

                if (fn) {
                        fn.one = true;
                }

                return this.on(name, fn);

        },

        /**
         * Triggers all callbacks for a given event name.
         * @param  {String} name Event name
         * @return {Object}      Reference to this Observable
         */
        trigger: function(name) {

                var i = 0,
                        args = Array.prototype.slice.call(arguments, 1),
                        fn,
                        fns = this.callbacks[name] || [];

                for (i; (fn = fns[i]); ++i) {

                        if (!fn.busy) {
                                fn.busy = true;
                                fn.apply(this, [name].concat(args));
                                if (fn.one) {
                                        fns.splice(i, 1);
                                        i--;
                                }
                                fn.busy = false;
                        }

                }

                return this;

        }

};

define(
        'EventEmitter',
        function() {

                var THIS_REGEX = /\bthis\b/,
                        slice = Array.prototype.slice;


                function extend(target, source) {
                        for (var key in source) {
                                if (source.hasOwnProperty(key)) {
                                        target[key] = source[key];
                                }
                        }

                        return target;
                }

                /**
                 * Invokes a function with the given arguments and context
                 * @param {function} fn - Function to be invoked
                 * @param {array} args - Array of arguments to pass to the function
                 * @param {object} context - Context under which to invoke the function
                 * @returns {mixed} The return value of the invoked function
                 */
                function invoke(fn, args, context) {
                        return context ? fn.call(context, args) : fn(args);
                }


                /**
                 * Checks if all filters listed in target also exist in test.
                 * @param {array} target - List of filters to match
                 * @param {array} target - List of filters to test
                 */
                function matchFilters(target, test) {
                        var i,
                                j;

                        target.sort();
                        test.sort();
 
                        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;
                }

                /**
                 * Parses and iterates an events string
                 * @param {object} context - Context to be passed to the iterator function
                 * @param {string} events - Events string to be parsed and iterated
                 * @param {function} fn - Iterator function called for each event topic
                 * @param {mixed} args - Additional arguments to be passed to the iterator
                 * @returns {object} The object passed as the context
                 */
                function iterateEvents(context, events, fn, args) {
                        events = events.split(' ');
                        var event,
                                filters,
                                i;

                        for (i = 0; (event = events[i]); i++) {
                                filters = event.split('.');
                                fn(context, filters.shift(), filters, args);
                        }

                        return context;
                }

                /**
                 * Removes an event handler
                 * @param {string} events - Event topics (separated by spaces) and filters (separated by dots)
                 * @returns {object} The object from which the events were removed
                 */
                function off(events) {
                        return iterateEvents(this, events, _off);
                }

                function _off(context, topic, filters) {
                        topic = context._topics[topic];
                        var handler,
                                i;

                        if (topic) {
                                if (!filters.length) {
                                        topic.length = 0;
                                } else {
                                        for (i = 0; (handler = topic[i]); i++) {
                                                if (matchFilters(filters, handler.filters)) {
                                                        topic.splice(j--, 1);
                                                }
                                        }
                                }
                        }
                }

                /**
                 * Attaches an event handler
                 * @param {string} events - Event topics (separated by spaces) and filters (separated by dots)
                 * @param {function} fn - Handler to be called when the event is triggered
                 * @returns {object} The object from which the events are attached
                 */
                function on(events, fn) {
                        return iterateEvents(this, events, _on, fn);
                }


                function _on(context, topic, filters, fn) {
                        if (!context._topics[topic]) {
                                context._topics[topic] = [];
                        }

                        context._topics[topic].push({
                                filters: filters,
                                fn: fn,
                                hasContext: THIS_REGEX.test(fn)
                        });
                }

                /**
                 * Attaches an event handler to be used only once
                 * @param {string} events - Event topics (separated by spaces) and filters (separated by dots)
                 * @param {function} fn - Handler to be called when the event is triggered
                 * @returns {object} The object from which the events are attached
                 */
                function one(events, fn) {
                        fn = function() {
                                this.off(fn);
                                fn.apply(this, arguments);
                        };
                        return on.apply(this, arguments);
                }


                /**
                 * Triggers all handlers associated with an event topic
                 * @param {string} topic - The name of the event topic to trigger
                 * @returns {object} The object from which the events are triggered
                 */
                function trigger(topic /* ... */) {
                        var handlers = this._topics[topic];

                        if (handlers) {
                                _trigger(this, handlers, slice.call(arguments, 1));
                        }

                        return this;
                }


                /**
                 * Calls a set of event handlers
                 * @param {object} context - The object which is firing the event
                 * @param {array} handlers - Array of the handlers to be invoked
                 * @param {array} args - Additional arguments to be passed to the handlers
                 */
                function _trigger(context, handlers, args) {
                        var handler,
                                stop,
                                i;

                        for (i = 0; (handler = handlers[i]); i++) {
                                if (invoke(handler.fn, args, handler.hasContext && context) === false) {
                                        break;
                                }
                        }
                }


                /**
                 * Mixin for the eventing behavior
                 * @param {object} obj - Object to have on/one/off/trigger mixed in
                 * @returns {object} The updated object
                 */
                return function(obj) {
                        if (obj !== Object(obj)) {
                                // TODO throw a good error
                        }

                        extend(obj, {
                                _topics: obj._topics || {},
                                off: off,
                                on: on,
                                one: one,
                                trigger: trigger
                        });

                        return obj;
                };
        }
);

function define(name, fn) {
EventEmitter = fn();
}
</script>

Setup

var backboneObserver = new Observer();
    var riotObserver = new Observable();
    var eventEmitter = EventEmitter({});
    
    function callback () {
        var string = 'Some string to operate on.',
                words = string.split(' '),
                longestWord;
    
        words.forEach(function(word) {
                if (!longestWord || word.length < longestWord.length) {
                        longestWord = word;
                }
        });
    
        console.log('Longest word is: %s', longestWord);
    }
    
    for (var i = 0; i < 20; i++) {
      backboneObserver.on('twentylisteners', callback);
      riotObserver.on('twentylisteners', callback);
      eventEmitter.on('twentylisteners', callback);
    }
    
    backboneObserver.on('onelistener', callback);
    riotObserver.on('onelistener', callback);
    eventEmitter.on('onelistener', callback);

Test runner

Ready to run.

Testing in
TestOps/sec
.trigger() Backbone
backboneObserver.trigger('twentylisteners');
backboneObserver.trigger('onelistener');
backboneObserver.trigger('test');
ready
.trigger() Riot.js
riotObserver.trigger('twentylisteners');
riotObserver.trigger('onelistener');
riotObserver.trigger('test');
ready
.trigger() EventEmitter
eventEmitter.trigger('twentylisteners');
eventEmitter.trigger('onelistener');
eventEmitter.trigger('test');
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 1: published by Drew on
  • Revision 2: published on
  • Revision 3: published by Drew on
  • Revision 4: published by Joshua Piccari on
  • Revision 5: published by Joshua Piccari on
  • Revision 6: published by Joshua Piccari on