A Comparison of JS Publish/Subscribe Approaches

JavaScript performance comparison

Revision 97 of this test case created by Brandon Papworth

Info

Changed

A Comparison JS Publish/Subscribe Approaches

In this comparison I'm trying to focus on event inheritance feature present in PubSubJS and my implementation Peter Higgins´ Port from Dojo.

In most PubSub implementations Subscribers and Publishers have a one to one relationship. This gives them a huge performance benefit, but can be a drawback when building complex decoupled applications as you have to wire every single event. In this comparison I'm looking at a pretty standard GUI like the one on google.com. We have a HEADER region, a tool region to the LEFT, a CONTENT region and a FOOTER. Each one of these regions are dependent on each other using PubSub to communicate. A MANAGER in each region is responsible for Loading/Unloading modules. A typical PubSub message for this application would look something like this:

"/APP/REGION/MODULE/EVENT"

Using a PubSub implementation that allows for inheritance allow us to create a subscriber for "/APP/REGION" that would listen to ALL events that occur within this region. Using the simpler implementations we would have to publish two events, one for the module and one for the region.

In this example each PubSub implementation has 4 subscribers. One for APP, REGION, MODULE and EVENT each. PubSubJS and JQuery Subscriber may invoke all four subscriber callbacks by a single publication to app/region/module/event where as the rest each has to publish 4 events.

More info

Compared:

Preparation code

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js?fastfix=me"></script>
<script>
  var jQueryFastfixed = jQuery.noConflict(true);
  delete window.jQuery;
  delete window.$;
</script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="//raw.github.com/hij1nx/EventEmitter2/master/lib/eventemitter2.js"></script>
<script src="//raw.github.com/pmelander/Subtopic/master/minified/subtopic.min.js"></script>
<script src="//raw.github.com/phiggins42/bloody-jquery-plugins/master/pubsub.js"></script>
<script src="//raw.github.com/appendto/amplify/master/src/core.js"></script>
<script src="//raw.github.com/spine/spine/dev/lib/spine.js"></script>
<script src="https://raw.github.com/rafikk/ply/master/src/core.js"></script>
<script src="//raw.github.com/mroderick/PubSubJS/master/src/pubsub.js"></script>
<script src="//raw.github.com/phiggins42/bloody-jquery-plugins/55e41df9bf08f42378bb08b93efcb28555b61aeb/pubsub.js"></script>

<script>
(function(window, $, undefined) {
// ## jquery/event/fastfix/fastfix.js

  // http://bitovi.com/blog/2012/04/faster-jquery-event-fix.html
  // https://gist.github.com/2377196

  // IE 8 has Object.defineProperty but it only defines DOM Nodes. According to
  // http://kangax.github.com/es5-compat-table/#define-property-ie-note
  // All browser that have Object.defineProperties also support Object.defineProperty properly
  if(Object.defineProperties) {
    var
      // Use defineProperty on an object to set the value and return it
      set = function (obj, prop, val) {
        if(val !== undefined) {
          Object.defineProperty(obj, prop, {
            value : val
          });
        }
        return val;
      },
      // special converters
      special = {
        pageX : function (original) {
          if(!original) {
            return;
          }

          var eventDoc = this.target.ownerDocument || document;
          doc = eventDoc.documentElement;
          body = eventDoc.body;
          return original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
        },
        pageY : function (original) {
          if(!original) {
            return;
          }

          var eventDoc = this.target.ownerDocument || document;
          doc = eventDoc.documentElement;
          body = eventDoc.body;
          return original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 );
        },
        relatedTarget : function (original) {
          if(!original) {
            return;
          }

          return original.fromElement === this.target ? original.toElement : original.fromElement;
        },
        metaKey : function (originalEvent) {
          if(!originalEvent) {
            return;
          }
          return originalEvent.ctrlKey;
        },
        which : function (original) {
          if(!original) {
            return;
          }

          return original.charCode != null ? original.charCode : original.keyCode;
        }
      };

    // Get all properties that should be mapped
    $.each($.event.keyHooks.props.concat($.event.mouseHooks.props).concat($.event.props), function (i, prop) {
      if (prop !== "target") {
        (function () {
          Object.defineProperty($.Event.prototype, prop, {
            get : function () {
              // get the original value, undefined when there is no original event
              var originalValue = this.originalEvent && this.originalEvent[prop];
              // overwrite getter lookup
              return this['_' + prop] !== undefined ? this['_' + prop] : set(this, prop,
                // if we have a special function and no value
                special[prop] && originalValue === undefined ?
                  // call the special function
                  special[prop].call(this, this.originalEvent) :
                  // use the original value
                  originalValue)
            },
            set : function (newValue) {
              // Set the property with underscore prefix
              this['_' + prop] = newValue;
            }
          });
        })();
      }
    });

    $.event.fix = function (event) {
      if (event[ $.expando ]) {
        return event;
      }
      // Create a jQuery event with at minimum a target and type set
      var originalEvent = event,
        event = $.Event(originalEvent);
      event.target = originalEvent.target;
      // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)
      if (!event.target) {
        event.target = originalEvent.srcElement || document;
      }

      // Target should not be a text node (#504, Safari)
      if (event.target.nodeType === 3) {
        event.target = event.target.parentNode;
      }

      return event;
    }
  }

 
})(this, jQueryFastfixed);
</script>

<script>
(function(context,OBJECT,NUMBER,LENGTH,toString,version,undefined,oldClass,jsface){function isMap(obj){return obj&&typeof obj===OBJECT&&!(typeof obj.length===NUMBER&&!obj.propertyIsEnumerable(LENGTH));}function isArray(obj){return obj&&typeof obj===OBJECT&&typeof obj.length===NUMBER&&!obj.propertyIsEnumerable(LENGTH);}function isFunction(obj){return obj&&typeof obj==="function";}function isFunction(obj){return obj&&typeof obj==="function";}function isString(obj){return toString.apply(obj)==="[object String]";}function isClass(obj){return isFunction(obj)&&obj.prototype&&obj===obj.prototype.constructor;}function copier(key,value,ignoredKeys,object,iClass,oPrototype){if(!ignoredKeys||!ignoredKeys.hasOwnProperty(key)){object[key]=value;if(iClass){oPrototype[key]=value;}}}function extend(object,subject,ignoredKeys){if(isArray(subject)){for(var len=subject.length;--len>=0;){extend(object,subject[len],ignoredKeys);}}else{ignoredKeys=ignoredKeys||{constructor:1,$super:1,prototype:1,$superb:1};var iClass=isClass(object),isSubClass=isClass(subject),oPrototype=object.prototype,supez,key,proto;if(isMap(subject)){for(key in subject){copier(key,subject[key],ignoredKeys,object,iClass,oPrototype);}}if(isSubClass){proto=subject.prototype;for(key in proto){copier(key,proto[key],ignoredKeys,object,iClass,oPrototype);}}if(iClass&&isSubClass){extend(oPrototype,subject.prototype,ignoredKeys);}}}function Class(parent,api){if(!api){parent=(api=parent,0);}var clazz,constructor,singleton,statics,key,bindTo,len,i=0,p,ignoredKeys={constructor:1,$singleton:1,$statics:1,prototype:1,$super:1,$superp:1,main:1},overload=Class.overload,plugins=Class.plugins;api=(typeof api==="function"?api():api)||{};constructor=api.hasOwnProperty("constructor")?api.constructor:0;singleton=api.$singleton;statics=api.$statics;for(key in plugins){ignoredKeys[key]=1;}clazz=singleton?{}:constructor?overload?overload("constructor",constructor):constructor:function(){};bindTo=singleton?clazz:clazz.prototype;parent=!parent||isArray(parent)?parent:[parent];len=parent&&parent.length;while(i<len){p=parent[i++];for(key in p){if(!ignoredKeys[key]){bindTo[key]=p[key];if(!singleton){clazz[key]=p[key];}}}for(key in p.prototype){if(!ignoredKeys[key]){bindTo[key]=p.prototype[key];}}}for(key in api){if(!ignoredKeys[key]){bindTo[key]=api[key];}}for(key in statics){clazz[key]=bindTo[key]=statics[key];}if(!singleton){p=parent&&parent[0]||parent;clazz.$super=p;clazz.$superp=p&&p.prototype?p.prototype:p;}for(key in plugins){plugins[key](clazz,parent,api);}if(isFunction(api.main)){api.main.call(clazz,clazz);}return clazz;}Class.plugins={};jsface={version:version,Class:Class,extend:extend,isMap:isMap,isArray:isArray,isFunction:isFunction,isString:isString,isClass:isClass};if(typeof module!=="undefined"&&module.exports){module.exports=jsface;}else{oldClass=context.Class;context.Class=Class;context.jsface=jsface;jsface.noConflict=function(){context.Class=oldClass;};}})(this,"object","number","length",Object.prototype.toString,"2.1.1");(function(context){var jsface=context.jsface,Class=jsface.Class,isFunction=jsface.isFunction,readyFns=[],readyCount=0;Class.plugins.$ready=function(clazz,parent,api){var r=api.$ready,len=parent?parent.length:0,count=len,pa,i,entry;while(len--){for(i=0;i<readyCount;i++){entry=readyFns[i];pa=parent[len];if(pa===entry[0]){entry[1].call(pa,clazz,parent,api);count--;}if(!count){break;}}}if(isFunction(r)){r.call(clazz,clazz,parent,api);readyFns.push([clazz,r]);readyCount++;}};})(this);var Utils=Class({$singleton:true,noop:function(){},asteriskEnd:function(string){return(string.slice(string.length-1)==="*");},removeAsteriskEnd:function(string){if(this.asteriskEnd(string)){return string.slice(0,string.length-1);}else{return false;}}});var PubSub2=Class(function(){var parseChannel=function(string){if(string.length){if(Utils.asteriskEnd(string)){return Utils.removeAsteriskEnd(string);}}};return{main:function(){this.modules=[];this.channels={};this.channelsList=[];this.subscriptions={};},$singleton:true,$statics:{async:function(fn){setTimeout(function(){fn();},0);},hasSubscribers:function(channel){return((this.channels[channel].subscribers).length>0);}},createChannel:function(channel){if(this.channels[channel]){return this;}else{this.channels[channel]=new Channel(channel,true);this.channelsList.push(channel);var len=this.channels[channel].subChannels.length,i=0;for(i=0;i<len;i++){if(!this.channels[this.channels[channel].subChannels[i]]){this.channels[this.channels[channel].subChannels[i]]=new Channel(this.channels[channel].subChannels[i],false);this.channelsList.push(this.channels[channel].subChannels[i]);}}return this;}},deliver:function(channel,data){var len=this.channels[channel].subscribers.length,i=0;for(i=0;i<len;i++){if(this.channels[channel].subscribers[i]){this.channels[channel].subscribers[i].callback(data);}}},publish:function(channel,data){var theChannel=this.channels[channel];if(theChannel.subscribers.length){var len=theChannel.subscribers.length,i;for(i=0;i<len;i++){if(theChannel.subscribers[i]){theChannel.subscribers[i].callback(data);}}if(theChannel.subChannels.length){len=theChannel.subChannels.length;for(i=0;i<len;i++){this.deliver(theChannel.subChannels[i],data);}}}},subscribe:function(channel,cb){if(Utils.asteriskEnd(channel)){channel=parseChannel(channel);}if(!this.channels[channel]){return null;}if(this.channels[channel].subscribers){this.channels[channel].subscribers.push({callback:cb});return this.channels[channel].subscribers.length-1;}else{return null;}},unsubscribe:function(channel,id){if(this.channels[channel].subscribers[id]){this.channels[channel].subscribers[id]=0;return this;}else{return this;}}};});var Channel=Class({constructor:function(channel,original){this.channel=channel;this.original=original;this.subscribers=[];this.splitter=":";if(this.original){this.subChannels=[];this.parseTopics();}},deleteChannel:function(){this.channel=null;this.original=null;this.subscribers=null;this.splitter=null;this.subChannels=null;this.parseTopics=null;this.clearSubs=null;this.changeChannel=null;return true;},clearSubs:function(){this.subscribers=[];},changeChannel:function(channel){PubSub2.publish("pubsub:channels:changeChannel",{prev:this.channel,next:channel},true);this.channel=channel;},parseTopics:function(){var colonIdx=this.channel.indexOf(":"),dotIdx=this.channel.indexOf(".");if(colonIdx>-1){this.splitter=":";}else{if(dotIdx>-1){this.splitter=".";}}var channelArr=this.channel.split(this.splitter);var len=channelArr.length,str="",i=0,x=0;for(i=0;i<(len-1);i++){str="";for(x=0;x<=i;x++){if(x===0){str=channelArr[x];}else{str=str+this.splitter+channelArr[x];}}this.subChannels.push(str);}}});
</script>

<script type="text/javascript">
  var iter = 0,
      callback = function () {
        iter += 1;
      },
      payload = {
        "somekey" : "somevalue"
      },
      x = 0,
      id = 0,
      id2 = 0,
      noop = function () {void(0);},
      emitter = null,
      emitterWITHwildcard = null,
      jQueryWindow = null,
      jQueryFastfixedWindow = null;

    jQuery(function () {
      emitter = new EventEmitter2({
        "wildcard"     : false,
        "delimiter"    : "/",
        "newListener"  : false,
        "maxListeners" : 0
      });
      emitterWildcard = new EventEmitter2({
        "wildcard"     : true,
        "delimiter"    : "/",
        "newListener"  : false,
        "maxListeners" : 0
      });

      PubSub2.createChannel('topic2');
      id = PubSub2.subscribe('topic2',noop);
      PubSub2.createChannel('topic5');
      PubSub2.createChannel('app');
      PubSub2.subscribe('app',callback);

      jQueryWindow = jQuery(window).on('app', callback);
      jQueryFastfixedWindow = jQueryFastfixed(window).on('app',callback);
      PubSub.subscribe('app', callback);
      subtopic.subscribe('app', callback);
      amplify.subscribe('app', callback);
      Spine.bind('app', callback);
      Ply.core.listen('app', callback);
      emitter.on('app',callback);
      emitterWildcard.on('app',callback);
    });
</script>

Preparation code output

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
jQuery Events
jQueryWindow.trigger('app', payload);
pending…
jQuery Events (Fastfixed)
jQueryFastfixedWindow.trigger('app', payload);
pending…
PubSubJS - async
PubSub.publish('app', payload);
pending…
PubSubJS - sync
PubSub.publishSync('app', payload);
pending…
Pure JS PubSub
Events.publish('app', [payload]);
pending…
Amplify Pub/Sub
amplify.publish('app', payload);
pending…
Spine Events
Spine.trigger('app', payload);
pending…
Ply Notify/Listen
Ply.core.notify('app', window, payload);
pending…
Subtopic
subtopic.publish('app', [payload]);
pending…
My-PubSub part of ControllerJS on github
PubSub2.publish('app',payload);
pending…
EventEmitter2
emitter.emit('app',payload)
pending…
EventEmitter2 with wildcard
emitterWildcard.emit('app',payload)
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:

0 comments

Add a comment