A Comparison of JS Publish/Subscribe Approaches (v97)

Revision 97 of this benchmark created by Brandon Papworth on


Description

Changed

  • fixed broken EventEmitter2 include
  • added external script references for applicable includes
  • upgraded to jQuery 2.0.0
  • cached jQuery window object as opposed to building it each loop iteration. Building it each time is inefficient and causes an unfair disadvantage.
  • added an extra version of jQuery with the faster "event.fix" added

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 HTML

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

Test runner

Ready to run.

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

Revisions

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