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

Revision 16 of this benchmark created 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;
};
var Event = (function () {
    function fetchListener(id, listeners) {
        var res;
        listeners.some(function (listener) {
            if (listener.id == id) {
                res = listener;
                return true;
            }
        });
        return res;
    }

    return Class({
        constructor: function (eventName) {
            this.listeners = [];
            this.id = eventName;
        },
        /**
         * @method addListener
         * @param {Object} listener
         * @param {String} listener.id
         * @param {Function} listener.content handler of this listener
         * @param {Boolean} [listener.force] override old content if same listener id exists
         * @param {Boolean} [listener.single] remove this listener after being called
         */
        addListener: function (listener) {
            var self = this;
            var force = (typeof(listener.force) == 'boolean') ? listener.force : false;
            var fetchedListener = fetchListener(listener.id, this.listeners);
            var returnFnc = function (lis) {
                return {
                    remove: function () {
                        self.removeListener(lis.id);
                    },
                    id: lis.id
                }
            };

            if (fetchedListener) {
                if (force) {
                    delete fetchedListener.content;
                    fetchedListener.content = listener.content;
                    return returnFnc(fetchedListener);
                    //return fetchListener[0];
                }
                console.warn("listener ID conflict: " + listener.id);
                return;
            }
            this.listeners.push(listener);
            return returnFnc(listener);
            //return listener;
        },
        /**
         * @method removeListener
         * @param {String} listenerId
         */
        removeListener: function (listenerId) {
            //delete this.listeners[listenerIndex];
            this.listeners.splice(this.findListenerIndex(listenerId), 1);
        },
        purgeListeners: function () {
            this.listeners.length = 0;
        },
        /**
         * @method findListener
         * @param {String} listenerId
         * @return Object|Null
         */
        findListener: function (listenerId) {
           return fetchListener(listenerId, this.listeners);
        },
        findListenerIndex: function (listenerId) {
            var index = null;
            this.listeners.some(function (listener, i) {
                if (listener.id == listenerId) {
                    index = i;
                    return true;
                }
            });
            return index;
        },
        /**
         * event trigger process:
         * <ul>
         * <li>if we fire an event without indicating listernId, all the listerners will be triggered;</li>
         * <li>if there is only one listener,return the result directly, otherwise,
         * the returned result will be like: [{id:'l1',result:result}]; </li>
         * <li>if we indicated listenerId,the result will be returned</li>
         * <li>if listener has "single == true", it will be triggered only once then removed.<li>
         * </ul>
         * @method fireListener
         * @param {String} listenerId if listenerId is specified, call only this listener
         * @param {Object} args arguments to be passed when firing a listener
         * @return {Object|Array} it will return the listener id and its result
         */
        fireListener: function (listenerId,args) {
            args = Array.isArray(args) ? args : [args];
            var res, self = this, re = [], del = [];
            if (!listenerId) {
                if (this.listeners.length == 1) {
                    res = this.listeners[0].content.apply(this, args);
                    if (this.listeners[0].single === true) {
                        this.listeners.splice(0, 1);
                    }
                    return res;
                }
                //if not, the returned result will be like: [{id:'l1',result:result}]
                this.listeners.forEach(function (listener) {
                    re.push({
                        id: listener.id,
                        result: listener.content.apply(self, args)
                    });

                    if (listener.single === true) {
                        del.push(listener.id);
                    }
                });
                del.forEach(function (d) {
                    this.listeners.splice(this.findListenerIndex(d), 1);
                }, this);
                return re;
            }
            //if we indicated listenerId,return directly the result
            var listener = this.findListener(listenerId);
            if (!listener) {
                console.warn("no such listener " + listenerId);
                return null;
            }
            //listener = listener[0];
            res = listener.content.apply(this, args);
            if (listener.single === true) {
                this.listeners.splice(this.findListenerIndex(listenerId), 1);
            }
            return res;
        },
        emit: function (listenerId, args) {
            return this.fireListener(listenerId,args);
        }
    });
})();
  /*    

 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
//http://jsperf.com/peter-higgins-pubsub-vs-tiny-pubsub/6
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;
        }
    };
})();
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)
        }
    });
})();

var dark={global:this,pubsub:{}};dark.pubsub.PubSub=function(){this._cache={}};dark.pubsub.PubSub.prototype.subscribe=function(c,d,a){this._cache[c]||(this._cache[c]=[]);return this._cache[c].push({func:d,context:a})};dark.pubsub.PubSub.prototype.publish=function(c,d){var a,b;a=this._cache[c];if(!a)return!1;for(b=a.length;b--;)void 0===a[b].context?a[b].func(d):a[b].func.call(a[b].context,d);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 CustomEvent();
    for (var i = 0; i < 1000; i++) {
      target.addListener("topic_" + i, function() {});
    }
    
    for (var i = 0; i < 1000; i++) {
      pubSub.subscribe('topic_' + i, function() {});
    }
    
    var target1 = new Observable();
    for (var i = 0; i < 1000; i++) {
      target1.on("topic_" + i, function() {});
    }
    
    var dpubsub = new dark.pubsub.PubSub();
    for (var i = 0; i < 1000; i++) {
      dpubsub.subscribe("topic_" + i, function() {});
    }

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
target.fire({ type: "topic_300" });
ready
pubsub simple
pubSub.publish('topic_300');
ready
Observable
target1.emit("topic_300");
ready
dpubsub
dpubsub.publish('topic_300');
ready

Revisions

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