A Comparison of JS Publish/Subscribe Approaches

JavaScript performance comparison

Revision 54 of this test case created

Info

A Comparison of JS Publish/Subscribe Approaches

More info: publish/subscribe on Wikipedia.

Compared:

Preparation code

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript">
</script>
<script src="https://raw.github.com/mroderick/PubSubJS/master/src/pubsub.js" type="text/javascript">
</script>
<script src="https://raw.github.com/kuwabarahiroshi/bloody-jquery-plugins/master/pubsub.js" type="text/javascript">
</script>
<script src="https://raw.github.com/phiggins42/bloody-jquery-plugins/55e41df9bf08f42378bb08b93efcb28555b61aeb/pubsub.js" type="text/javascript">
</script>
<script src="https://raw.github.com/gist/1695338/736bcf94f9226d975fce3ad4f2bc08ccbf176bad/pubsub.js" type="text/javascript">
</script>
<script src="https://raw.github.com/appendto/amplify/master/core/amplify.core.js" type="text/javascript">
</script>
<script src="https://raw.github.com/maccman/spine/master/lib/spine.js" type="text/javascript">
</script>
<script src="https://raw.github.com/richardscarrott/ply/master/src/core.js" type="text/javascript">
</script>
<script src="https://raw.github.com/jrburke/requirejs/master/require.js" type="text/javascript">
</script>
<script type="text/javascript">
window.callback = function () {};
window.payload = {
  somekey: 'some value'
};
var Observer = jQuery({});

jQuery(function() {
 require(["https://raw.github.com/jkroso/Observer/master/lib/Observer.js"], function(Hyraki){
  window.hyraki = new Hyraki()
  for (var i = 0; i < 10; i++) {
    Observer.on('my-event-' + i, callback);
    PubSub.subscribe('my-event-' + i, callback);
    jQuery.subscribe('my-event-' + i, callback);
    Events.subscribe('my-event-' + i, callback);
    App.subscribe('my-event-' + i, callback);
    amplify.subscribe('my-event-' + i, callback);
    Spine.bind('my-event-' + i, callback);
    Ply.core.listen('my-event-' + i, callback);
    hyraki.on('my-event-' + i, callback);
  }
})
});

</script>
<script>
(function () { window.archi = window.archi || {}; }());
/*
  config = {
    templateDir: '/templates/', // null/undefined if you don't want to dynamically include the templates
    // if a templateDir is NOT specified (you don't want to use dynamic templates), and your modules will be using
    //  templates, you need to define them here so the Core can pass them to the modules. If both the templateDir and
    //  the templates properties are defined in the config option, the Core will treat the templates object that's passed
    //  here as the one with priority.
    templates: {
      'name': 'templateName',
      'template': 'template'
    },
    extDir: '/js/', // where is the extensions are located
    moduleDir: '/js/modules/', // null/undefined if you don't want to dynamically include the modules
    apiBase: '', // null/undefined if none
    apiKey: '', // null/undefined if none
    sessionId: '', // null/undefined if none
    extensions: []
  };
*/

(function (win, doc, ns, undef) {
  "use strict";

  // cover needed methods (from MDN)
  if (!Array.prototype.some) {
    Array.prototype.some = function(fun /*, thisp */) {
      if (this === null)
        throw new TypeError();
   
      var t = new Object(this);
      var len = t.length >>> 0;
      if (typeof fun != "function")
        throw new TypeError();
   
      var thisp = arguments[1];
      for (var i = 0; i < len; i++) {
        if (i in t && fun.call(thisp, t[i], i, t))
          return true;
      }
   
      return false;
    };
  }

  if (!Array.prototype.filter) {
    Array.prototype.filter = function(fun /*, thisp */) {
      if (this === null)
        throw new TypeError();
   
      var t = new Object(this);
      var len = t.length >>> 0;
      if (typeof fun != "function")
        throw new TypeError();
   
      var res = [];
      var thisp = arguments[1];
      for (var i = 0; i < len; i++) {
        if (i in t) {
          var val = t[i]; // in case fun mutates this
          if (fun.call(thisp, val, i, t))
            res.push(val);
        }
      }
   
      return res;
    };
  }

  var Core = (function (conf) {
    var Public = {extensions: []},
      Private = {
        templateDir: conf && Private.addSlash(conf.templateDir) || null,
        modulesDir: conf && Private.addSlash(conf.moduleDir) || null,
        extDir: conf && Private.addSlash(conf.extDir) || null,
        apiBase: conf && conf.apiBase || null,
        apiKey: conf && conf.apiKey || null,
        sid: conf && conf.sessionId || null,
        debug: true,
        modules: {},
        expects: {}
      };

    Public.extend = function (name, callback) {
      var params = [win, doc, Public, Private, ns.Sanbox], method, methods = [], extension;

      extension = callback.call(Public, Public);

      for (method in extension) {
        if (extension.hasOwnProperty(method) && typeof extension[method] === 'function') {
          methods.push(method);
          Public[method] = extension[method];
        }
      }

      Public.extensions.push({'name' : name, 'methods' : methods});

      return Public; // chainable
    };

    Public.register = function (moduleName, creator) {
      Private.modules[moduleName] = {
        create: creator,
        instance: null
      };

      return Public; // chainable
    };
   
    Public.start = function (moduleName) {
      var module = Private.modules[moduleName];

      module.instance = module.create(ns.Sandbox);
      module.instance.init(null);

      return Public; // chainable
    };

    Public.stop = function (moduleName) {
      var module = Private.modules[moduleName];
      if (module) {
        if (module.instance.hasOwnProperty('destroy') && typeof module.instance.destroy == 'function') {
          module.instance.destroy();
        }
        module.instance = null;
      }

      return Public; // chainable
    };

    Public.startAll = function () {
      var modules = Private.modules,
        moduleName;

      for (moduleName in modules) {
        if (modules.hasOwnProperty(moduleName)) {
          Public.start(moduleName);
        }
      }

      return Public; // chainable
    };

    Public.stopAll = function () {
      var modules = Private.modules,
        moduleName;

      for (moduleName in modules) {
        if (modules.hasOwnProperty(moduleName)) {
          Public.stop(moduleName);
        }
      }

      return Public; // chainable
    };

    return Public;
  }());

  win.Core = ns.Core = Core;
}(window, document, window.archi)); // return the global object - don't want to assume 'window'
(function (win, Core) {
  "use strict";
  Core.extend('communication', function (Public, undef) {
    var Extension = {},
      Private = {};

    Private.eventPool = {};

    Extension.notify = function (eventInfo) {
      // the setTimeout is here to allow for the listeners to catch up on events that are
      // fired right away, regardless of the order in which the event listeners / notifiers
      // are called. In short it allows you to call the notify method before you call the
      // listen method the listen method will still fire.

      var eventName = eventInfo.name,
        eventData = eventInfo.response,
        callback = eventInfo.callback;

      win.setTimeout(function () {
        var listenerCount = 0, eventLen, e;

        if (eventName in Private.eventPool) {
          Private.eventPool[eventName].some(function (listener, idx) {
            listener.callback.apply(listener.scope, [eventInfo]);
            listenerCount++;
          });
        }

        // callback for notifier
        if (typeof callback === 'function') {
          callback(listenerCount, eventData);
        }
      }, 0);

      return Public; // chainable
    };

    Extension.listen = function (eventName, callback, scope) {
      var i, len, event;

      if (!(eventName instanceof Array)) {
        eventName = [eventName];
      }

      for (i = 0, len = eventName.length; i < len; i++) {
        event = Private.eventPool[eventName[i]] || [];
        event.push({'callback': callback, 'scope': scope || null});
        Private.eventPool[eventName[i]] = event;
      }

      return Public; // chainable
    };

    Extension.forget = function (eventName, callback) {
      var i, j, nLen, eLen, event;

      if (!(eventName instanceof Array)) {
        eventName = [eventName];
      }

      for (i = 0, nLen = eventName.length; i < nLen; i += 1) {
        event = eventName[i];
        if (Private.eventPool[event] && Private.eventPool[event] instanceof Array) {
          if (callback === undef) {
            Private.eventPool[event] = [];
          } else {
            // listenOnce was called
            Private.eventPool[event] = Private.eventPool[event].filter(function(eventObj){
              return eventObj.callback !== callback;
            });
          }
        }
      }

      return Public; // chainable
    };

    Extension.listenOnce = function (eventName, callback, scope) {
      var fireAndForget = function () {
        callback.apply(Public, arguments);
        Public.forget(eventName, fireAndForget);
      };

      Public.listen(eventName, fireAndForget, scope);

      return Public; // chainable
    };

    return Extension;
  });
}(window, archi.Core));
</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
Observer.trigger('my-event-9', payload);
 
pending…
PubSubJS
PubSub.publish('my-event-9', payload);
pending…
jQuery PubSub plugin
$.publish('my-event-9', [payload]);
pending…
Pure JS PubSub
Events.publish('my-event-9', [payload]);
pending…
Darcy Clarke
App.publish('my-event-9', [payload]);
pending…
Amplify Pub/Sub
amplify.publish('my-event-9', payload);
pending…
Spine Events
Spine.trigger('my-event-9', payload);
pending…
Ply Notify/Listen
Ply.core.notify('my-event-9', window, payload);
pending…
hyraki
hyraki.publish('my-event-9', payload);
pending…
archi
archi.Core.notify('my-event-9', 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