Peter Higgins Pubsub vs tiny-pubsub VS simple pubsub VS Observable (v8)

Revision 8 of this benchmark created by HK on


Preparation HTML

<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js">
</script>
<script>
function Class(params) {
    var extend = params.extend || function () {
        },
        mixin = function (dest, source) {
            var name;
            for (name in source) {
                dest[name] = (typeof(source[name]) !== 'function' || typeof parent[name] !== "function") ? source[name] :
                    (function (name, fn) {
                        return function () {
                            var tmp = this._super;
                            this._super = parent[name];
                            var ret = fn.apply(this, arguments);
                            this._super = tmp;

                            return ret;
                        };
                    })(name, source[name]);
            }
            return dest;
        }, parent = extend.prototype;
    params.__construct = params.constructor || function () {
    };
    delete params.constructor;
    delete params.extend;
    function Constructor() {
        this.__construct && this.__construct.apply(this, arguments);
    }

    if (extend) {
        Constructor.prototype = Object.create(extend.prototype);
    }
    mixin(Constructor.prototype, params);
    Constructor.prototype.constructor = Constructor;
    return Constructor;
}

  /*    

 jQuery pub/sub plugin by Peter Higgins (dante@dojotoolkit.org)

 Loosely based on Dojo publish/subscribe API, limited in scope. Rewritten blindly.

 Original is (c) Dojo Foundation 2004-2010. Released under either AFL or new BSD, see:
 http://dojofoundation.org/license for more information.

 */

  ;
  (function(d) {

    // the topic/subscription hash
    var cache = {};

    d.publish = function( /* String */ topic, /* Array? */ args) {
      // summary:
      //                Publish some data on a named topic.
      // topic: String
      //                The channel to publish on
      // args: Array?
      //                The data to publish. Each array item is converted into an ordered
      //                arguments on the subscribed functions.
      //
      // example:
      //                Publish stuff on '/some/topic'. Anything subscribed will be called
      //                with a function signature like: function(a,b,c){ ... }
      //
      //        |               $.publish("/some/topic", ["a","b","c"]);
      cache[topic] && d.each(cache[topic], function() {
        this.apply(d, args || []);
      });
    };

    d.subscribe = function( /* String */ topic, /* Function */ callback) {
      // summary:
      //                Register a callback on a named topic.
      // topic: String
      //                The channel to subscribe to
      // callback: Function
      //                The handler event. Anytime something is $.publish'ed on a
      //                subscribed channel, the callback will be called with the
      //                published array as ordered arguments.
      //
      // returns: Array
      //                A handle which can be used to unsubscribe this particular subscription.
      //
      // example:
      //        |       $.subscribe("/some/topic", function(a, b, c){ /* handle data */ });
      //
      if (!cache[topic]) {
        cache[topic] = [];
      }
      cache[topic].push(callback);
      return [topic, callback]; // Array
    };

    d.unsubscribe = function( /* Array */ handle) {
      // summary:
      //                Disconnect a subscribed function for a topic.
      // handle: Array
      //                The return value from a $.subscribe call.
      // example:
      //        |       var handle = $.subscribe("/something", function(){});
      //        |       $.unsubscribe(handle);
      var t = handle[0];
      cache[t] && d.each(cache[t], function(idx) {
        if (this == handle[1]) {
          cache[t].splice(idx, 1);
        }
      });
    };

  })(jQuery);



  (function($) {

    var o = $({});

    $.sub = function() {
      o.on.apply(o, arguments);
    };

    $.unsub = function() {
      o.off.apply(o, arguments);
    };

    $.pub = function() {
      o.trigger.apply(o, arguments);
    };

  }(jQuery));

/*!
 * Amplify Core 1.1.0
 * 
 * Copyright 2011 appendTo LLC. (http://appendto.com/team)
 * Dual licensed under the MIT or GPL licenses.
 * http://appendto.com/open-source-licenses
 * 
 * http://amplifyjs.com
 */
(function( global, undefined ) {

var slice = [].slice,
        subscriptions = {};

var amplify = global.amplify = {
        publish: function( topic ) {
                var args = slice.call( arguments, 1 ),
                        topicSubscriptions,
                        subscription,
                        length,
                        i = 0,
                        ret;

                if ( !subscriptions[ topic ] ) {
                        return true;
                }

                topicSubscriptions = subscriptions[ topic ].slice();
                for ( length = topicSubscriptions.length; i < length; i++ ) {
                        subscription = topicSubscriptions[ i ];
                        ret = subscription.callback.apply( subscription.context, args );
                        if ( ret === false ) {
                                break;
                        }
                }
                return ret !== false;
        },

        subscribe: function( topic, context, callback, priority ) {
                if ( arguments.length === 3 && typeof callback === "number" ) {
                        priority = callback;
                        callback = context;
                        context = null;
                }
                if ( arguments.length === 2 ) {
                        callback = context;
                        context = null;
                }
                priority = priority || 10;

                var topicIndex = 0,
                        topics = topic.split( /\s/ ),
                        topicLength = topics.length,
                        added;
                for ( ; topicIndex < topicLength; topicIndex++ ) {
                        topic = topics[ topicIndex ];
                        added = false;
                        if ( !subscriptions[ topic ] ) {
                                subscriptions[ topic ] = [];
                        }
        
                        var i = subscriptions[ topic ].length - 1,
                                subscriptionInfo = {
                                        callback: callback,
                                        context: context,
                                        priority: priority
                                };
        
                        for ( ; i >= 0; i-- ) {
                                if ( subscriptions[ topic ][ i ].priority <= priority ) {
                                        subscriptions[ topic ].splice( i + 1, 0, subscriptionInfo );
                                        added = true;
                                        break;
                                }
                        }

                        if ( !added ) {
                                subscriptions[ topic ].unshift( subscriptionInfo );
                        }
                }

                return callback;
        },

        unsubscribe: function( topic, callback ) {
                if ( !subscriptions[ topic ] ) {
                        return;
                }

                var length = subscriptions[ topic ].length,
                        i = 0;

                for ( ; i < length; i++ ) {
                        if ( subscriptions[ topic ][ i ].callback === callback ) {
                                subscriptions[ topic ].splice( i, 1 );
                                break;
                        }
                }
        }
};

}( this ) );


//Copyright (c) 2010 Nicholas C. Zakas. All rights reserved.
//MIT License
var CustomEvent = new Class({
    constructor: function() {
        this._listeners = {};
    },
    addListener: function(type, listener) {
        if (typeof this._listeners[type] === "undefined") {
            this._listeners[type] = [];
        }
        this._listeners[type].push(listener);
    },
    fire: function(event) {
        if (typeof event === "string") {
            event = {type: event};
        }
        if (!event.target) {
            event.target = this;
        }
        if (!event.type) {  //falsy
            throw new Error("Event object missing 'type' property.");
        }

        if (this._listeners[event.type] instanceof Array) {
            var listeners = this._listeners[event.type];
            for (var i = 0, len = listeners.length; i < len; i++) {
                listeners[i].call(this, event);
            }
        }
    },
    removeListener: function(type, listener) {
        if (this._listeners[type] instanceof Array) {
            var listeners = this._listeners[type];
            for (var i = 0, len = listeners.length; i < len; i++) {
                if (listeners[i] === listener) {
                    listeners.splice(i, 1);
                    break;
                }
            }
        }
    },
    hasListeners: function(type) {
        if (this._listeners[type] instanceof Array) {
            return (this._listeners[type].length > 0);
        } else {
            return false;
        }
    },
    getListeners:function(type) {
        if (this._listeners[type] instanceof Array) {
            return this._listeners[type];
        }
    }
});
var pubSub = (function() {

    var topics = {},
        subUid = -1;

    return {
        publish: function(topic, args) {

            if (!topics[topic]) {
                return false;
            }

            var subscribers = topics[topic],
                    len = subscribers ? subscribers.length : 0;

            while (len--) {
                subscribers[len].func(topic, args);
            }

            return this;
        },
        subscribe: function(topic, func) {

            if (!topics[topic]) {
                topics[topic] = [];
            }

            var token = (++subUid).toString();
            topics[topic].push({
                token: token,
                func: func
            });
            return token;
        },
        unsubscribe: function(token) {
            for (var m in topics) {
                if (topics[m]) {
                    for (var i = 0, j = topics[m].length; i < j; i++) {
                        if (topics[m][i].token === token) {
                            topics[m].splice(i, 1);
                            return token;
                        }
                    }
                }
            }
            return this;
        }
    };
})();
</script>

Setup

for (var i = 0; i < 1000; i++) {
      $.subscribe('topic_' + i, function() {});
    }
    
    for (var i = 0; i < 1000; i++) {
      $.sub('topic_' + i, function() {});
    }
    
    for (var i = 0; i < 1000; i++) {
      amplify.subscribe('topic_' + i, function() {});
    }
    
    var target = new EventTarget();
    for (var i = 0; i < 1000; i++) {
      target.addListener("topic_", function() {});
    }
    
    for (var i = 0; i < 1000; i++) {
      pubSub.subscribe('topic_' + i, function() {});
    }
    
    /**
     * Observable pattern.
     * @class PlusNode.helpers.Observable
     * @constructor
     * @requires PlusNode.helpers.Event
     */
    var Observable = (function(){
        var isType = function (compare) {
            if (typeof compare === 'string' && /^\w+$/.test(compare)) {
                compare = '[object ' + compare + ']';
            } else {
                compare = Object.prototype.toString.call(compare);
            }
            return isType[compare] || (isType[compare] = function (o) {
                return Object.prototype.toString.call(o) === compare;
            });
        };
        return Class({
            constructor:function () {
                this.events = {};
            },
            /**
             * call example: <pre class='brush:js'>
             *  fireEvent('eventName');
             *  fireEvent('eventName',[arguments]);
             *  fireEvent('eventName','listenerId',[arguments]);
             * </pre>
             * @method fireEvent
             * @param {String} eventName
             * @param {String} listenerId
             * @param {Object|String} args arguments passed to listener
             */
            fireEvent:function (eventName, listenerId, args) {
                if (!this.events[eventName]) {
                    return console.warn("no such event ID: " + eventName);
                }
                //no specific listenerId,fire all the listeners
                if (typeof(listenerId) != "string") {
                    if (typeof(args) == "undefined") {
                        //if there are only 2 arguments, the second one is the argument to pass when event got fired
                        args = listenerId;
                    }
    
                    return this.events[eventName].fireListener(null,args);
                }
                //has specific listenerId, fire this listener
                if (!this.events[eventName].findListener(listenerId)) {
                    return console.warn("no such listener ID: " + listenerId + " for event " + eventName);
                }
                return this.events[eventName].fireListener(listenerId,args);
    
            },
            emit:function () {
                return this.fireEvent.apply(this, arguments)
            },
            /**
             * @example
             * listeners example:
             * <pre class='brush:js'>
             * listeners : [{
                 *  id:'listener1',
                 *  content:function(args){//do sth. }
                 * }]
             * </pre>
             * @method addEvent
             * @param {String} eventName
             * @param {Array} listeners
             * @return {Array} list of added listeners
             */
            addEvent:function (eventName, listeners) {
                var l = [];
                eventName = eventName.trim();
                if (!this.events[eventName]) {
                    this.events[eventName] = new Event(eventName);
                }
                if (isType('Function')(listeners)) {
                    listeners = this.listenerToArray(listeners);
                }
                if (isType('Object')(listeners)) {
                    listeners = this.listenersToArray(listeners);
                }
                if (Array.isArray(listeners)) {
                    listeners.forEach(function (listener) {
                        l.push(this.events[eventName].addListener(listener));
                    }, this);
                }
                if (l.length == 1) {
                    return l.shift();
                }
                l.remove = function () {
                    this.forEach(function (el) {
                        if (!el || !el.remove) {
                            return;
                        }
                        el.remove();
                    });
                };
                return l;
            },
            listenersToArray:function (listeners) {
                var li = [];
                Object.keys(listeners).forEach(function (name) {
                    li.push({
                        id:name,
                        content:listeners[name]
                    });
                });
                return li
            },
            listenerToArray:function (listener) {
                return [
                    {
                        id:"#Listener:" + (new Date()).getTime() + Math.floor(Math.random() * 100),
                        content:listener
                    }
                ];
            },
            once:function (event, listeners) {
                if (isType('Function')(listeners)) {
                    listeners = this.listenerToArray(listeners);
                }
                listeners = listeners.map(function (item) {
                    item.single = true;
                    return item;
                });
                return this.addEvent(event, listeners);
            },
            on:function () {
                return this.addEvent.apply(this, arguments);
            },
            /**
             * detect if has an event of a listener
             * @method hasEvent
             * @param {String} eventName
             * @param {String} listenerId (optional) if listenerId is defined, return this listener or false
             * @return {false|Object}
             */
            hasEvent:function (eventName, listenerId) {
                var hasEvent = this.events[eventName] || false;
                if (hasEvent && listenerId) {
                    return hasEvent.findListener(listenerId) || false;
                }
                return hasEvent;
            },
            /**
             * @method removeEvent
             * @param {String} eventName
             * @param {String} listenerId
             */
            removeEvent:function (eventName, listenerId) {
                if (!this.events[eventName]) {
                    console.warn("no such event ID: " + eventName);
                    return;
                }
                if (!listenerId) {
                    this.events[eventName].purgeListeners();
                    delete this.events[eventName];
                    return;
                }
                return this.events[eventName].removeListener(listenerId);
            },
            off:function () {
                return this.removeEvent.apply(this, arguments)
            }
        });
    })();

Test runner

Ready to run.

Testing in
TestOps/sec
Peter Higgins
$.publish('topic_300');
ready
Tiny Pubsub
$.pub('topic_300');
ready
Amplify
amplify.publish('topic_300');
ready
Nicholas C. Zakas JS Custom Events
var target = new CustomEvent();
target.fire({ type: "topic_300" });
ready
pubsub simple
pubSub.publish('topic_300');
ready
Observable
var observable = new Observable();
observable.fireEvent('topic_300');
ready

Revisions

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