PubSubJS vs. jQuery custom events (v101)

Revision 101 of this benchmark created on


Description

An attempt at showing that PubSubJS is faster than using jQuery custom evens for publish/subscribe style messaging.

It's certainly not as rich in features, and I am happy with that.

Introducing PubSubJS

Preparation HTML

<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>
/*! Tiny Pub/Sub - v0.7.0 - 2013-01-29
* https://github.com/cowboy/jquery-tiny-pubsub
* Copyright (c) 2013 "Cowboy" Ben Alman; Licensed MIT */
(function($) {

  var o = $({});

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

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

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

}(jQuery));

/*
Copyright (c) 2010,2011,2012 Morgan Roderick http://roderick.dk
License: MIT - http://mrgnrdrck.mit-license.org

https://github.com/mroderick/PubSubJS
*/
/*jslint white:true, plusplus:true, stupid:true*/
/*global
        setTimeout,
        module,
        exports,
        define,
        require,
        window
*/

(function(root, factory){
        'use strict';

        // CommonJS
        if (typeof exports === 'object'){
                module.exports = factory();

        // AMD
        } else if (typeof define === 'function' && define.amd){
                define(factory);
        // Browser
        } else {
                root.PubSub = factory();
        }
}( ( typeof window === 'object' && window ) || this, function(){

        'use strict';

        var PubSub = {
                        name: 'PubSubJS',
                        version: '1.3.5'
                },
                messages = {},
                lastUid = -1;

        /**
         *      Returns a function that throws the passed exception, for use as argument for setTimeout
         *      @param { Object } ex An Error object
         */
        function throwException( ex ){
                return function reThrowException(){
                        throw ex;
                };
        }

        function callSubscriberWithDelayedExceptions( subscriber, message, data ){
                try {
                        subscriber( message, data );
                } catch( ex ){
                        setTimeout( throwException( ex ), 0);
                }
        }

        function callSubscriberWithImmediateExceptions( subscriber, message, data ){
                subscriber( message, data );
        }

        function deliverMessage( originalMessage, matchedMessage, data, immediateExceptions ){
                var subscribers = messages[matchedMessage],
                        callSubscriber = immediateExceptions ? callSubscriberWithImmediateExceptions : callSubscriberWithDelayedExceptions,
                        i, j;

                if ( !messages.hasOwnProperty( matchedMessage ) ) {
                        return;
                }

                for ( i = 0, j = subscribers.length; i < j; i++ ){
                        callSubscriber( subscribers[i].func, originalMessage, data );
                }
        }

        function createDeliveryFunction( message, data, immediateExceptions ){
                return function deliverNamespaced(){
                        var topic = String( message ),
                                position = topic.lastIndexOf( '.' );

                        // deliver the message as it is now
                        deliverMessage(message, message, data, immediateExceptions);

                        // trim the hierarchy and deliver message to each level
                        while( position !== -1 ){
                                topic = topic.substr( 0, position );
                                position = topic.lastIndexOf('.');
                                deliverMessage( message, topic, data );
                        }
                };
        }

        function messageHasSubscribers( message ){
                var topic = String( message ),
                        found = messages.hasOwnProperty( topic ),
                        position = topic.lastIndexOf( '.' );

                while ( !found && position !== -1 ){
                        topic = topic.substr( 0, position );
                        position = topic.lastIndexOf('.');
                        found = messages.hasOwnProperty( topic );
                }

                return found;
        }

        function publish( message, data, sync, immediateExceptions ){
                var deliver = createDeliveryFunction( message, data, immediateExceptions ),
                        hasSubscribers = messageHasSubscribers( message );

                if ( !hasSubscribers ){
                        return false;
                }

                if ( sync === true ){
                        deliver();
                } else {
                        setTimeout( deliver, 0 );
                }
                return true;
        }

        /**
         *      PubSub.publish( message[, data] ) -> Boolean
         *      - message (String): The message to publish
         *      - data: The data to pass to subscribers
         *      Publishes the the message, passing the data to it's subscribers
        **/
        PubSub.publish = function( message, data ){
                return publish( message, data, false, PubSub.immediateExceptions );
        };

        /**
         *      PubSub.publishSync( message[, data] ) -> Boolean
         *      - message (String): The message to publish
         *      - data: The data to pass to subscribers
         *      Publishes the the message synchronously, passing the data to it's subscribers
        **/
        PubSub.publishSync = function( message, data ){
                return publish( message, data, true, PubSub.immediateExceptions );
        };

        /**
         *      PubSub.subscribe( message, func ) -> String
         *      - message (String): The message to subscribe to
         *      - func (Function): The function to call when a new message is published
         *      Subscribes the passed function to the passed message. Every returned token is unique and should be stored if
         *      you need to unsubscribe
        **/
        PubSub.subscribe = function( message, func ){
                // message is not registered yet
                if ( !messages.hasOwnProperty( message ) ){
                        messages[message] = [];
                }

                // forcing token as String, to allow for future expansions without breaking usage
                // and allow for easy use as key names for the 'messages' object
                var token = String(++lastUid);
                messages[message].push( { token : token, func : func } );

                // return token for unsubscribing
                return token;
        };

        /**
         *      PubSub.unsubscribe( tokenOrFunction ) -> String | Boolean
         *  - tokenOrFunction (String|Function): The token of the function to unsubscribe or func passed in on subscribe
         *  Unsubscribes a specific subscriber from a specific message using the unique token
         *  or if using Function as argument, it will remove all subscriptions with that function
        **/
        PubSub.unsubscribe = function( tokenOrFunction ){
                var isToken = typeof tokenOrFunction === 'string',
                        key = isToken ? 'token' : 'func',
                        succesfulReturnValue = isToken ? tokenOrFunction : true,

                        result = false,
                        m, i;

                for ( m in messages ){
                        if ( messages.hasOwnProperty( m ) ){
                                for ( i = messages[m].length-1 ; i >= 0; i-- ){
                                        if ( messages[m][i][key] === tokenOrFunction ){
                                                messages[m].splice( i, 1 );
                                                result = succesfulReturnValue;

                                                // tokens are unique, so we can just return here
                                                if ( isToken ){
                                                        return result;
                                                }
                                        }
                                }
                        }
                }

                return result;
        };

        return PubSub;
}));

/*! pubbie 2013-03-28 */
(function(){(function(n){var t,e,i,r;return t={},r=-1,i={},e={subscribe:function(n,e){var u;return t.hasOwnProperty(n)||(t[n]=[]),u=r+=1,t[n].push({id:u,fn:e}),i[u]={namespace:n,index:t[n].length-1},u},publish:function(n,e){var i,r,u,s,c;for(r=t[n]||[],u=function(n){return n.fn.apply(this,e||[])},s=0,c=r.length;c>s;s++)i=r[s],u(i);return this},unsubscribe:function(n){var e,r;return e="string"==typeof n,e?t[n]=[]:(r=i[n],t[r.namespace].splice(r.index,1)),this},reset:function(){return t={},r=-1,this}},n.pubbie=n.pubbie||e})(window,window.document)}).call(this);

(function( global, undefined ) {

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

var amplify = global.amplify = {
        publish: function( topic ) {
                if ( typeof topic !== "string" ) {
                        throw new Error( "You must provide a valid topic to publish." );
                }

                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 ( typeof topic !== "string" ) {
                        throw new Error( "You must provide a valid topic to create a subscription." );
                }

                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 ( typeof topic !== "string" ) {
                        throw new Error( "You must provide a valid topic to remove a subscription." );
                }

                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 ) );

/*!
* Pub/Sub implementation
* http://addyosmani.com/
* Licensed under the GPL
* http://jsfiddle.net/LxPrq/
*/


;(function ( window, doc, undef ) {

    var topics = {},
        subUid = -1,
        pubsubz ={};

    pubsubz.publish = function ( topic, args ) {

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

        setTimeout(function () {
            var subscribers = topics[topic],
                len = subscribers ? subscribers.length : 0;

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

        return true;

    };

    pubsubz.subscribe = function ( topic, func ) {

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

        var token = (++subUid).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };

    pubsubz.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 false;
    };

    getPubSubz = function(){
        return pubsubz;
    };

    window.pubsubz = getPubSubz();

}( this, this.document ));

</script>
<script>
var jcallback = function (e, args, def) {
def.resolve();
}
  var callback1 = function(args, def) {
    def.resolve();
  };
  var payload = {
   somekey: 'some value'
  };
for(var i = 0, l = 100; i < l; i++) {
$.subscribe('event' + i, jcallback);
PubSub.subscribe('event' + i, callback1);
pubbie.subscribe('event' + i, callback1);
amplify.subscribe('event' + i, callback1);
pubsubz.subscribe('event' + i, callback1);
}

</script>

Test runner

Ready to run.

Testing in
TestOps/sec
jQuery DOM - trigger
return false;
ready
jQuery Object - trigger
// async test
for(var i=0,l=100;i<l;i++) {
$.publish('event' + i, [payload, deferred]);
}
ready
PubSub - publish - asyncronous
// async test
for(var i=0,l=100;i<l;i++) {
PubSub.publish('event' + i, [payload, deferred]);
}
ready
PubSub - publish - syncronous
// async test
for(var i=0,l=100;i<l;i++) {
PubSub.publish('event' + i, [payload, deferred], true);
}
ready
jQuery document - trigger
return false;
ready
pubbie
// async test
for(var i=0,l=100;i<l;i++) {
pubbie.publish('event' + i, [payload, deferred]);
}
ready
amplify
// async test
for(var i=0,l=100;i<l;i++) {
amplify.publish('event' + i, [payload, deferred]);
}
ready
pubsubz
// async test
for(var i=0,l=100;i<l;i++) {
pubsubz.publish('event' + i, [payload, deferred]);
}
ready

Revisions

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