A Comparison of JS Publish/Subscribe Approaches (v130)

Revision 130 of this benchmark created on


Description

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="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript">
</script>
<script type="text/javascript">
var subtopic=(function(){var b={},c=function(f,e,h){var k=f.split("/");while(k[0]){if(b[f]){var j=b[f],g=j.length-1;for(g;g>=0;g-=1){j[g].apply(h||this,e||[])}}k.pop();f=k.join("/")
}},a=function(e,f){if(!b[e]){b[e]=[]}b[e].push(f);return[e,f]},d=function(g,h){var f=g[0],e=b[f].length-1;if(b[f]){for(e;e>=0;e-=1){if(b[f][e]===g[1]){b[f].splice(b[f][e],1);if(h){delete b[f]
}}}}};return{publish:c,subscribe:a,unsubscribe:d}}());
</script>
<script type="text/javascript">
/**
         *      Events. Pub/Sub system for Loosely Coupled logic.
         *      Based on Peter Higgins' port from Dojo to jQuery
         *      https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js
         *
         *      Re-adapted to vanilla Javascript
         *
         *      @class Events
         */
        var Events = (function (){
                var cache = {},
                        /**
                         *      Events.publish
                         *      e.g.: Events.publish("/Article/added", [article], this);
                         *
                         *      @class Events
                         *      @method publish
                         *      @param topic {String}
                         *      @param args     {Array}
                         *      @param scope {Object} Optional
                         */
                        publish = function (topic, args, scope) {
                                if (cache[topic]) {
                                        var thisTopic = cache[topic],
                                                i = thisTopic.length - 1;

                                        for (i; i >= 0; i -= 1) {
                                                thisTopic[i].apply( scope || this, args || []);
                                        }
                                }
                        },
                        /**
                         *      Events.subscribe
                         *      e.g.: Events.subscribe("/Article/added", Articles.validate)
                         *
                         *      @class Events
                         *      @method subscribe
                         *      @param topic {String}
                         *      @param callback {Function}
                         *      @return Event handler {Array}
                         */
                        subscribe = function (topic, callback) {
                                if (!cache[topic]) {
                                        cache[topic] = [];
                                }
                                cache[topic].push(callback);
                                return [topic, callback];
                        },
                        /**
                         *      Events.unsubscribe
                         *      e.g.: var handle = Events.subscribe("/Article/added", Articles.validate);
                         *              Events.unsubscribe(handle);
                         *
                         *      @class Events
                         *      @method unsubscribe
                         *      @param handle {Array}
                         *      @param completly {Boolean}
                         *      @return {type description }
                         */
                        unsubscribe = function (handle, completly) {
                                var t = handle[0],
                                        i = cache[t].length - 1;

                                if (cache[t]) {
                                        for (i; i >= 0; i -= 1) {
                                                if (cache[t][i] === handle[1]) {
                                                        cache[t].splice(cache[t][i], 1);
                                                        if(completly){ delete cache[t]; }
                                                }
                                        }
                                }
                        };

                return {
                        publish: publish,
                        subscribe: subscribe,
                        unsubscribe: unsubscribe
                };
}());
</script>
<script type="text/javascript">
/*!
 * Amplify Core @VERSION
 *
 * 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 ) {
                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 ) );
</script>
<script type="text/javascript">
// Generated by CoffeeScript 1.3.3
(function() {
  var $, Controller, Events, Log, Model, Module, Spine, createObject, isArray, isBlank, makeArray, moduleKeywords,
    __slice = [].slice,
    __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
    __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
    __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

  Events = {
    bind: function(ev, callback) {
      var calls, evs, name, _i, _len;
      evs = ev.split(' ');
      calls = this.hasOwnProperty('_callbacks') && this._callbacks || (this._callbacks = {});
      for (_i = 0, _len = evs.length; _i < _len; _i++) {
        name = evs[_i];
        calls[name] || (calls[name] = []);
        calls[name].push(callback);
      }
      return this;
    },
    one: function(ev, callback) {
      return this.bind(ev, function() {
        this.unbind(ev, arguments.callee);
        return callback.apply(this, arguments);
      });
    },
    trigger: function() {
      var args, callback, ev, list, _i, _len, _ref;
      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      ev = args.shift();
      list = this.hasOwnProperty('_callbacks') && ((_ref = this._callbacks) != null ? _ref[ev] : void 0);
      if (!list) {
        return;
      }
      for (_i = 0, _len = list.length; _i < _len; _i++) {
        callback = list[_i];
        if (callback.apply(this, args) === false) {
          break;
        }
      }
      return true;
    },
    unbind: function(ev, callback) {
      var cb, i, list, _i, _len, _ref;
      if (!ev) {
        this._callbacks = {};
        return this;
      }
      list = (_ref = this._callbacks) != null ? _ref[ev] : void 0;
      if (!list) {
        return this;
      }
      if (!callback) {
        delete this._callbacks[ev];
        return this;
      }
      for (i = _i = 0, _len = list.length; _i < _len; i = ++_i) {
        cb = list[i];
        if (!(cb === callback)) {
          continue;
        }
        list = list.slice();
        list.splice(i, 1);
        this._callbacks[ev] = list;
        break;
      }
      return this;
    }
  };

  Log = {
    trace: true,
    logPrefix: '(App)',
    log: function() {
      var args;
      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      if (!this.trace) {
        return;
      }
      if (this.logPrefix) {
        args.unshift(this.logPrefix);
      }
      if (typeof console !== "undefined" && console !== null) {
        if (typeof console.log === "function") {
          console.log.apply(console, args);
        }
      }
      return this;
    }
  };

  moduleKeywords = ['included', 'extended'];

  Module = (function() {

    Module.include = function(obj) {
      var key, value, _ref;
      if (!obj) {
        throw new Error('include(obj) requires obj');
      }
      for (key in obj) {
        value = obj[key];
        if (__indexOf.call(moduleKeywords, key) < 0) {
          this.prototype[key] = value;
        }
      }
      if ((_ref = obj.included) != null) {
        _ref.apply(this);
      }
      return this;
    };

    Module.extend = function(obj) {
      var key, value, _ref;
      if (!obj) {
        throw new Error('extend(obj) requires obj');
      }
      for (key in obj) {
        value = obj[key];
        if (__indexOf.call(moduleKeywords, key) < 0) {
          this[key] = value;
        }
      }
      if ((_ref = obj.extended) != null) {
        _ref.apply(this);
      }
      return this;
    };

    Module.proxy = function(func) {
      var _this = this;
      return function() {
        return func.apply(_this, arguments);
      };
    };

    Module.prototype.proxy = function(func) {
      var _this = this;
      return function() {
        return func.apply(_this, arguments);
      };
    };

    function Module() {
      if (typeof this.init === "function") {
        this.init.apply(this, arguments);
      }
    }

    return Module;

  })();

  Model = (function(_super) {

    __extends(Model, _super);

    Model.extend(Events);

    Model.records = {};

    Model.crecords = {};

    Model.attributes = [];

    Model.configure = function() {
      var attributes, name;
      name = arguments[0], attributes = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
      this.className = name;
      this.records = {};
      this.crecords = {};
      if (attributes.length) {
        this.attributes = attributes;
      }
      this.attributes && (this.attributes = makeArray(this.attributes));
      this.attributes || (this.attributes = []);
      this.unbind();
      return this;
    };

    Model.toString = function() {
      return "" + this.className + "(" + (this.attributes.join(", ")) + ")";
    };

    Model.find = function(id) {
      var record;
      record = this.records[id];
      if (!record && ("" + id).match(/c-\d+/)) {
        return this.findCID(id);
      }
      if (!record) {
        throw new Error('Unknown record');
      }
      return record.clone();
    };

    Model.findCID = function(cid) {
      var record;
      record = this.crecords[cid];
      if (!record) {
        throw new Error('Unknown record');
      }
      return record.clone();
    };

    Model.exists = function(id) {
      try {
        return this.find(id);
      } catch (e) {
        return false;
      }
    };

    Model.refresh = function(values, options) {
      var record, records, _i, _len;
      if (options == null) {
        options = {};
      }
      if (options.clear) {
        this.records = {};
        this.crecords = {};
      }
      records = this.fromJSON(values);
      if (!isArray(records)) {
        records = [records];
      }
      for (_i = 0, _len = records.length; _i < _len; _i++) {
        record = records[_i];
        record.id || (record.id = record.cid);
        this.records[record.id] = record;
        this.crecords[record.cid] = record;
      }
      this.trigger('refresh', this.cloneArray(records));
      return this;
    };

    Model.select = function(callback) {
      var id, record, result;
      result = (function() {
        var _ref, _results;
        _ref = this.records;
        _results = [];
        for (id in _ref) {
          record = _ref[id];
          if (callback(record)) {
            _results.push(record);
          }
        }
        return _results;
      }).call(this);
      return this.cloneArray(result);
    };

    Model.findByAttribute = function(name, value) {
      var id, record, _ref;
      _ref = this.records;
      for (id in _ref) {
        record = _ref[id];
        if (record[name] === value) {
          return record.clone();
        }
      }
      return null;
    };

    Model.findAllByAttribute = function(name, value) {
      return this.select(function(item) {
        return item[name] === value;
      });
    };

    Model.each = function(callback) {
      var key, value, _ref, _results;
      _ref = this.records;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(callback(value.clone()));
      }
      return _results;
    };

    Model.all = function() {
      return this.cloneArray(this.recordsValues());
    };

    Model.first = function() {
      var record;
      record = this.recordsValues()[0];
      return record != null ? record.clone() : void 0;
    };

    Model.last = function() {
      var record, values;
      values = this.recordsValues();
      record = values[values.length - 1];
      return record != null ? record.clone() : void 0;
    };

    Model.count = function() {
      return this.recordsValues().length;
    };

    Model.deleteAll = function() {
      var key, value, _ref, _results;
      _ref = this.records;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(delete this.records[key]);
      }
      return _results;
    };

    Model.destroyAll = function() {
      var key, value, _ref, _results;
      _ref = this.records;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(this.records[key].destroy());
      }
      return _results;
    };

    Model.update = function(id, atts, options) {
      return this.find(id).updateAttributes(atts, options);
    };

    Model.create = function(atts, options) {
      var record;
      record = new this(atts);
      return record.save(options);
    };

    Model.destroy = function(id, options) {
      return this.find(id).destroy(options);
    };

    Model.change = function(callbackOrParams) {
      if (typeof callbackOrParams === 'function') {
        return this.bind('change', callbackOrParams);
      } else {
        return this.trigger('change', callbackOrParams);
      }
    };

    Model.fetch = function(callbackOrParams) {
      if (typeof callbackOrParams === 'function') {
        return this.bind('fetch', callbackOrParams);
      } else {
        return this.trigger('fetch', callbackOrParams);
      }
    };

    Model.toJSON = function() {
      return this.recordsValues();
    };

    Model.fromJSON = function(objects) {
      var value, _i, _len, _results;
      if (!objects) {
        return;
      }
      if (typeof objects === 'string') {
        objects = JSON.parse(objects);
      }
      if (isArray(objects)) {
        _results = [];
        for (_i = 0, _len = objects.length; _i < _len; _i++) {
          value = objects[_i];
          _results.push(new this(value));
        }
        return _results;
      } else {
        return new this(objects);
      }
    };

    Model.fromForm = function() {
      var _ref;
      return (_ref = new this).fromForm.apply(_ref, arguments);
    };

    Model.recordsValues = function() {
      var key, result, value, _ref;
      result = [];
      _ref = this.records;
      for (key in _ref) {
        value = _ref[key];
        result.push(value);
      }
      return result;
    };

    Model.cloneArray = function(array) {
      var value, _i, _len, _results;
      _results = [];
      for (_i = 0, _len = array.length; _i < _len; _i++) {
        value = array[_i];
        _results.push(value.clone());
      }
      return _results;
    };

    Model.idCounter = 0;

    Model.uid = function(prefix) {
      var uid;
      if (prefix == null) {
        prefix = '';
      }
      uid = prefix + this.idCounter++;
      if (this.exists(uid)) {
        uid = this.uid(prefix);
      }
      return uid;
    };

    function Model(atts) {
      Model.__super__.constructor.apply(this, arguments);
      if (atts) {
        this.load(atts);
      }
      this.cid = this.constructor.uid('c-');
    }

    Model.prototype.isNew = function() {
      return !this.exists();
    };

    Model.prototype.isValid = function() {
      return !this.validate();
    };

    Model.prototype.validate = function() {};

    Model.prototype.load = function(atts) {
      var key, value;
      for (key in atts) {
        value = atts[key];
        if (typeof this[key] === 'function') {
          this[key](value);
        } else {
          this[key] = value;
        }
      }
      return this;
    };

    Model.prototype.attributes = function() {
      var key, result, _i, _len, _ref;
      result = {};
      _ref = this.constructor.attributes;
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        key = _ref[_i];
        if (key in this) {
          if (typeof this[key] === 'function') {
            result[key] = this[key]();
          } else {
            result[key] = this[key];
          }
        }
      }
      if (this.id) {
        result.id = this.id;
      }
      return result;
    };

    Model.prototype.eql = function(rec) {
      return !!(rec && rec.constructor === this.constructor && (rec.cid === this.cid) || (rec.id && rec.id === this.id));
    };

    Model.prototype.save = function(options) {
      var error, record;
      if (options == null) {
        options = {};
      }
      if (options.validate !== false) {
        error = this.validate();
        if (error) {
          this.trigger('error', error);
          return false;
        }
      }
      this.trigger('beforeSave', options);
      record = this.isNew() ? this.create(options) : this.update(options);
      this.trigger('save', options);
      return record;
    };

    Model.prototype.updateAttribute = function(name, value, options) {
      this[name] = value;
      return this.save(options);
    };

    Model.prototype.updateAttributes = function(atts, options) {
      this.load(atts);
      return this.save(options);
    };

    Model.prototype.changeID = function(id) {
      var records;
      records = this.constructor.records;
      records[id] = records[this.id];
      delete records[this.id];
      this.id = id;
      return this.save();
    };

    Model.prototype.destroy = function(options) {
      if (options == null) {
        options = {};
      }
      this.trigger('beforeDestroy', options);
      delete this.constructor.records[this.id];
      delete this.constructor.crecords[this.cid];
      this.destroyed = true;
      this.trigger('destroy', options);
      this.trigger('change', 'destroy', options);
      this.unbind();
      return this;
    };

    Model.prototype.dup = function(newRecord) {
      var result;
      result = new this.constructor(this.attributes());
      if (newRecord === false) {
        result.cid = this.cid;
      } else {
        delete result.id;
      }
      return result;
    };

    Model.prototype.clone = function() {
      return createObject(this);
    };

    Model.prototype.reload = function() {
      var original;
      if (this.isNew()) {
        return this;
      }
      original = this.constructor.find(this.id);
      this.load(original.attributes());
      return original;
    };

    Model.prototype.toJSON = function() {
      return this.attributes();
    };

    Model.prototype.toString = function() {
      return "<" + this.constructor.className + " (" + (JSON.stringify(this)) + ")>";
    };

    Model.prototype.fromForm = function(form) {
      var key, result, _i, _len, _ref;
      result = {};
      _ref = $(form).serializeArray();
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        key = _ref[_i];
        result[key.name] = key.value;
      }
      return this.load(result);
    };

    Model.prototype.exists = function() {
      return this.id && this.id in this.constructor.records;
    };

    Model.prototype.update = function(options) {
      var clone, records;
      this.trigger('beforeUpdate', options);
      records = this.constructor.records;
      records[this.id].load(this.attributes());
      clone = records[this.id].clone();
      clone.trigger('update', options);
      clone.trigger('change', 'update', options);
      return clone;
    };

    Model.prototype.create = function(options) {
      var clone, record;
      this.trigger('beforeCreate', options);
      if (!this.id) {
        this.id = this.cid;
      }
      record = this.dup(false);
      this.constructor.records[this.id] = record;
      this.constructor.crecords[this.cid] = record;
      clone = record.clone();
      clone.trigger('create', options);
      clone.trigger('change', 'create', options);
      return clone;
    };

    Model.prototype.bind = function(events, callback) {
      var binder, unbinder,
        _this = this;
      this.constructor.bind(events, binder = function(record) {
        if (record && _this.eql(record)) {
          return callback.apply(_this, arguments);
        }
      });
      this.constructor.bind('unbind', unbinder = function(record) {
        if (record && _this.eql(record)) {
          _this.constructor.unbind(events, binder);
          return _this.constructor.unbind('unbind', unbinder);
        }
      });
      return binder;
    };

    Model.prototype.one = function(events, callback) {
      var binder,
        _this = this;
      return binder = this.bind(events, function() {
        _this.constructor.unbind(events, binder);
        return callback.apply(_this, arguments);
      });
    };

    Model.prototype.trigger = function() {
      var args, _ref;
      args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      args.splice(1, 0, this);
      return (_ref = this.constructor).trigger.apply(_ref, args);
    };

    Model.prototype.unbind = function() {
      return this.trigger('unbind');
    };

    return Model;

  })(Module);

  Controller = (function(_super) {

    __extends(Controller, _super);

    Controller.include(Events);

    Controller.include(Log);

    Controller.prototype.eventSplitter = /^(\S+)\s*(.*)$/;

    Controller.prototype.tag = 'div';

    function Controller(options) {
      this.release = __bind(this.release, this);

      var key, value, _ref;
      this.options = options;
      _ref = this.options;
      for (key in _ref) {
        value = _ref[key];
        this[key] = value;
      }
      if (!this.el) {
        this.el = document.createElement(this.tag);
      }
      this.el = $(this.el);
      this.$el = this.el;
      if (this.className) {
        this.el.addClass(this.className);
      }
      if (this.attributes) {
        this.el.attr(this.attributes);
      }
      if (!this.events) {
        this.events = this.constructor.events;
      }
      if (!this.elements) {
        this.elements = this.constructor.elements;
      }
      if (this.events) {
        this.delegateEvents(this.events);
      }
      if (this.elements) {
        this.refreshElements();
      }
      Controller.__super__.constructor.apply(this, arguments);
    }

    Controller.prototype.release = function() {
      this.trigger('release');
      this.el.remove();
      return this.unbind();
    };

    Controller.prototype.$ = function(selector) {
      return $(selector, this.el);
    };

    Controller.prototype.delegateEvents = function(events) {
      var eventName, key, match, method, selector, _results,
        _this = this;
      _results = [];
      for (key in events) {
        method = events[key];
        if (typeof method === 'function') {
          method = (function(method) {
            return function() {
              method.apply(_this, arguments);
              return true;
            };
          })(method);
        } else {
          if (!this[method]) {
            throw new Error("" + method + " doesn't exist");
          }
          method = (function(method) {
            return function() {
              _this[method].apply(_this, arguments);
              return true;
            };
          })(method);
        }
        match = key.match(this.eventSplitter);
        eventName = match[1];
        selector = match[2];
        if (selector === '') {
          _results.push(this.el.bind(eventName, method));
        } else {
          _results.push(this.el.delegate(selector, eventName, method));
        }
      }
      return _results;
    };

    Controller.prototype.refreshElements = function() {
      var key, value, _ref, _results;
      _ref = this.elements;
      _results = [];
      for (key in _ref) {
        value = _ref[key];
        _results.push(this[value] = this.$(key));
      }
      return _results;
    };

    Controller.prototype.delay = function(func, timeout) {
      return setTimeout(this.proxy(func), timeout || 0);
    };

    Controller.prototype.html = function(element) {
      this.el.html(element.el || element);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.append = function() {
      var e, elements, _ref;
      elements = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      elements = (function() {
        var _i, _len, _results;
        _results = [];
        for (_i = 0, _len = elements.length; _i < _len; _i++) {
          e = elements[_i];
          _results.push(e.el || e);
        }
        return _results;
      })();
      (_ref = this.el).append.apply(_ref, elements);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.appendTo = function(element) {
      this.el.appendTo(element.el || element);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.prepend = function() {
      var e, elements, _ref;
      elements = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      elements = (function() {
        var _i, _len, _results;
        _results = [];
        for (_i = 0, _len = elements.length; _i < _len; _i++) {
          e = elements[_i];
          _results.push(e.el || e);
        }
        return _results;
      })();
      (_ref = this.el).prepend.apply(_ref, elements);
      this.refreshElements();
      return this.el;
    };

    Controller.prototype.replace = function(element) {
      var previous, _ref;
      _ref = [this.el, $(element.el || element)], previous = _ref[0], this.el = _ref[1];
      previous.replaceWith(this.el);
      this.delegateEvents(this.events);
      this.refreshElements();
      return this.el;
    };

    return Controller;

  })(Module);

  $ = (typeof window !== "undefined" && window !== null ? window.jQuery : void 0) || (typeof window !== "undefined" && window !== null ? window.Zepto : void 0) || function(element) {
    return element;
  };

  createObject = Object.create || function(o) {
    var Func;
    Func = function() {};
    Func.prototype = o;
    return new Func();
  };

  isArray = function(value) {
    return Object.prototype.toString.call(value) === '[object Array]';
  };

  isBlank = function(value) {
    var key;
    if (!value) {
      return true;
    }
    for (key in value) {
      return false;
    }
    return true;
  };

  makeArray = function(args) {
    return Array.prototype.slice.call(args, 0);
  };

  Spine = this.Spine = {};

  if (typeof module !== "undefined" && module !== null) {
    module.exports = Spine;
  }

  Spine.version = '1.0.8';

  Spine.isArray = isArray;

  Spine.isBlank = isBlank;

  Spine.$ = $;

  Spine.Events = Events;

  Spine.Log = Log;

  Spine.Module = Module;

  Spine.Controller = Controller;

  Spine.Model = Model;

  Module.extend.call(Spine, Events);

  Module.create = Module.sub = Controller.create = Controller.sub = Model.sub = function(instances, statics) {
    var result;
    result = (function(_super) {

      __extends(result, _super);

      function result() {
        return result.__super__.constructor.apply(this, arguments);
      }

      return result;

    })(this);
    if (instances) {
      result.include(instances);
    }
    if (statics) {
      result.extend(statics);
    }
    if (typeof result.unbind === "function") {
      result.unbind();
    }
    return result;
  };

  Model.setup = function(name, attributes) {
    var Instance;
    if (attributes == null) {
      attributes = [];
    }
    Instance = (function(_super) {

      __extends(Instance, _super);

      function Instance() {
        return Instance.__super__.constructor.apply(this, arguments);
      }

      return Instance;

    })(this);
    Instance.configure.apply(Instance, [name].concat(__slice.call(attributes)));
    return Instance;
  };

  Spine.Class = Module;

}).call(this);
</script>
<script type="text/javascript">
/*global Ply, jQuery */
/*jshint eqeqeq: true, curly: true, white: true */

// **Ply** is a lightweight JavaScript framework for creating
// reusable UI components and managing application logic. It
// aims to eliminate boilerplate code, create a consistent interface,
// and provide common patterns for code organization and re-use.

// Ply is not an MVC framework and doesn't aim to compete
// with [Backbone.js](http://documentcloud.github.com/backbone/) or [SproutCore](http://www.sproutcore.com/). Instead, it is intended for users
// who need to maintain logic on the server and are looking for a better
// option for code re-use and organization.

// This file comprises the Core module.

// Declare global namespace and assign version number.

window.Ply = {
    VERSION: '0.3.4'
};

// Define `core` module.

Ply.core = (function ($) {

    // ## Private Variables
    // Create private variables. `listeners` is an associative array holding arrays of
    // notification listeners keyed on the notification name.
    var listeners = {},
        // id used to uniquely identify listeners for use in ignore.
        id        = 0,
        debug     = false;

    // ## Public Methods/Properties
    return {

        // ### Notify
        // Notifies listeners of an event. Notifiers should send themselves
        // and optional data as arguments.
        notify: function (note, sender, data) {

            // Cache listeners array or create a new array, assign, and cache it.
            var list = listeners[note] || (listeners[note] = []),
                // Create loop variables.
                i    = 0,
                len  = list.length;

            // Loop over listeners and notify each.
            for (; i < len; i++) {
                list[i].handler.call(list[i].listener, note, sender, data);
            }

        },

        // ### Listen
        // Listens for a particular notification or set of notifications.
        // Clients should pass in a handler function and themselves as arguments.
        // When the handler function is called, it will be applied to the `listener`'s
        // scope, ensuring that `this` refers to what the client expects.
        listen: function (notification, handler, listener) {

            // Cache the notification's listeners if it exists or create and cache
            // a new array otherwise.
            var list  = listeners[notification] || (listeners[notification] = []),
                // Split the notification on whitespace. Clients can listen to
                // multiple notifications by passing in a string with the notification
                // names split by whitespace.
                notes = notification.split(/\s/),
                // Create loop variables.
                len   = notes.length,
                i     = 0;

            // If the notification name contains whitespace,
            // listen on each particular notification (segment).
            if (len > 1) {
                for (; i < len; i++) {
                    this.listen(notes[i], handler, listener);
                }

                return;
            }

            // Add the listener and handler function to the notifications array.
            list.push({
                id: id += 1,
                handler: handler,
                listener: listener
            });
            
            // return handle used to ignore.
            return [notification, id];
        },

        // ### Ignore
        // Removes a particular notification from listeners object.
        // Clients should pass in the returned handle from `Ply.core.listen`.
        ignore: function (handle) {
            
            var note = handle[0];

            listeners[note] && $.each(listeners[note], function(i) {
                if(this.id == handle[1]){
                    listeners[note].splice(i, 1);
                }
            });

            return;
        },

        // ### Log
        // Lightweight logging wrapper around `console`. Useful less so
        // for debugging than for posting notices of interest.
        log: function (msg, type) {

            // Do nothing if debug mode is disabled.
            if (!debug) {
                return;
            }

            // Use the correct logging mechanism based
            // on the parameter type.
            if (window.console) {

                switch (type) {
                case 'warn':
                    console.warn(msg);
                    break;

                case 'info':
                    console.info(msg);
                    break;

                default:
                    console.log(msg);
                    break;
                }
            }

        },

        // ### Error
        // Method to catch JavaScript errors. All Ply methods are run in a `try/catch` block,
        // with exceptions being passed to this method.
        error: function (ex, sev) {

            // If an `onError` callback function is defined in `Ply.config.core`, call it.
            // Note that implementing this callback is highly recommended. See the sample implementation
            // in `Ply.config`.
            if (Ply.config.core.onError && typeof Ply.config.core.onError === 'function') {
                // Pass in the exception and the severity to the handler.
                Ply.config.core.onError(ex, sev);

                return;
            }

            // If no error handler is defined, simply throw the error.
            throw ex;

        },

        // ### Debug
        // Enabled debugging when called with no argues or with any truthy value. To disable debug mode
        // send `false` or another falsy value to this function, e.g.: `Ply.core.debug(0)`.
        debug: function (val) {

            // Default to `true` if no arguments.
            debug = typeof val === 'undefined' ? true : val;

            if (debug) {
                // Print debug notice.
                this.log('debugging...', 'info');

                // Manually set `this.debugOn` in case debug is set after Ply has been initialized.
                // `debugOn` will still be cached to the old value.
                this.debugOn = true;
            }
            else {
                // Do the opposite.
                this.debugOn = false;
            }
        },

        // ### Debug on
        // Cache the value of debug. This is used by clients to determine
        // if debug mode is currently enabled.
        debugOn: function () {
            // Coerce the value to a boolean.
            return !!debug;
        }()

    };

// Alias `jQuery` to `$` in module scope.
})(jQuery);

// &#8618; [Ajax](ajax.html)
</script>
<script type="text/javascript">
/*
Copyright (c) 2010,2011,2012 Morgan Roderick http://roderick.dk
License: MIT - http://mrgnrdrck.mit-license.org

https://github.com/mroderick/PubSubJS
*/
/*global
        setTimeout,
        module,
        exports,
        define,
        window
*/
(function (name, global, definition){
        "use strict";
        if (typeof module !== 'undefined'){
                module.exports = definition(name, global);
        } else if (typeof define === 'function' && typeof define.amd  === 'object'){
                define(definition);     
        } else {
                global[name] = definition(name, global);        
        } 
}('PubSub', ( typeof window !== 'undefined' && window ) || this, function definition(name, global){

        "use strict";
        
        var PubSub = {
                        name: 'PubSubJS',
                        version: '1.3.1-dev'
                },
                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 callSubscriber( subscriber, message, data ){
                try {
                        subscriber( message, data );
                } catch( ex ){
                        setTimeout( throwException( ex ), 0);
                }
        }

        function deliverMessage( originalMessage, matchedMessage, data ){
                var subscribers = messages[matchedMessage],
                        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 ){
                return function deliverNamespaced(){
                        var topic = String( message ),
                                position = topic.lastIndexOf( '.' );

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

                        // 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 ){
                var deliver = createDeliveryFunction( message, data ),
                        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.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.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, j;
                
                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;
}));
</script>
<script>
(function(scope) {
        'use strict';
        var pubsubInstance = null;
        var pubsubConfig = null;

        if(typeof pubsub === 'object') {
                pubsubConfig = pubsub;
        //node.js config from global
        } else if(typeof global === 'object' && typeof global.pubsubConfig === 'object') {
                pubsubConfig = global.pubsubConfig;
        }

        function Pubsub(config) {
                var _eventObject = {};
                var options = {
                        separator : (config && config.separator) ?  config.separator : '/',
                        recurrent : (config && typeof config.recurrent === 'boolean') ?  config.recurrent :  (false),
                        async     : (config && typeof config.async === 'boolean') ?  config.async :  (false),
                        log       : (config && config.log) ?  config.log :  (false)
                };

                function forEach(dataArray, callback) {
                        var i = 0,
                                arrayLength = dataArray.length;

                        for(i = 0; i < arrayLength; i++) {
                                callback(i);
                        }
                }

                function executeCallback(subscriptions, args, async) {
                        async = (typeof async === 'boolean') ?  async : options.async;

                        //clone array - callbacks can unsubscribe other subscriptions
                        var executedSubscriptions = subscriptions.slice();

                        forEach(executedSubscriptions, function(subscribtionId) {
                                var subscribtion = null;

                                if(typeof executedSubscriptions[subscribtionId] === 'object' && executedSubscriptions.hasOwnProperty(subscribtionId)) {
                                        subscribtion = executedSubscriptions[subscribtionId];
                                        if(async) {
                                                setTimeout(function() {
                                                        subscribtion.callback.apply(subscribtion.object, args);
                                                }, 4);
                                        } else {
                                                subscribtion.callback.apply(subscribtion.object, args);
                                        }
                                }
                        });
                }

                function executeWildcard(nsObject, args) {
                        var nsElement;
                        for(nsElement in nsObject) {
                                if(nsElement !== '_events' && nsObject.hasOwnProperty(nsElement)) {
                                        executeCallback(nsObject[nsElement]._events, args);
                                }
                        }
                }

                function subscribe(ns_string, callback, givenObject) {
                        var parts = ns_string.split(options.separator),
                                nsObject, //Namespace object to which we attach event
                                givenObjectSet = (givenObject) ? true : false,
                                eventObject = null,
                                i = 0;

                        givenObject = (givenObjectSet) ? givenObject : callback;

                        //Iterating through _eventObject to find proper nsObject
                        nsObject = _eventObject;
                        for (i = 0; i < parts.length; i += 1) {
                                if (typeof nsObject[parts[i]] === "undefined") {
                                        nsObject[parts[i]] = {};
                                        nsObject[parts[i]]._events = [];
                                }
                                nsObject = nsObject[parts[i]];
                        }

                        eventObject = {
                                callback        : callback,
                                object          : givenObject // "this" parameter in executed function
                        };

                        nsObject._events.push(eventObject);
                        return {namespace : parts.join(options.separator),
                                event : eventObject };
                }

                function unsubscribe (subscribeObject) {
                        var ns_string = subscribeObject.namespace,
                                eventObject = subscribeObject.event,
                                parts = ns_string.split(options.separator),
                                nsObject,
                                i = 0;

                        //Iterating through _eventObject to find proper nsObject
                        nsObject = _eventObject;
                        for (i = 0; i < parts.length; i += 1) {
                                if (typeof nsObject[parts[i]] === "undefined") {
                                        if(options.log) {
                                                console.error('There is no ' + ns_string + ' subscription');
                                        }
                                        return null;
                                }
                                nsObject = nsObject[parts[i]];
                        }

                        forEach(nsObject._events, function(eventId){
                                if(nsObject._events[eventId] === eventObject) {
                                        nsObject._events.splice(eventId, 1);
                                }
                        });
                }

                return {
                        /**
                         * Publish event
                         * @param ns_string string namespace string splited by dots
                         * @param args array of arguments given to callbacks
                         * @param recurrent bool should execution be bubbled throught namespace
                         * @param depth integer how many namespaces separated by dots will be executed
                         */
                        publish : function(ns_string, args, params) {
                                var that = this,
                                        parts = ns_string.split(options.separator),
                                        recurrent = (typeof params === 'object' && params.recurrent) ? params.recurrent : options.recurrent, // bubbles event throught namespace if true
                                        depth = (typeof params === 'object' && params.depth) ? params.depth : null,
                                        async = (typeof params === 'object' && params.async) ? params.async : options.async,
                                        nsObject, //Namespace object to which we attach event
                                        partsLength = parts.length,
                                        iPart = null,
                                        i;

                                args = (args) ? args : [],

                                nsObject = _eventObject;
                                for (i = 0; i < partsLength; i++) {
                                        iPart = parts[i];
                                        if(iPart === '*') {
                                                executeWildcard(nsObject, args, async);
                                                return null;
                                        } else if (typeof nsObject[iPart] === "undefined") {
                                                if(options.log) {
                                                        console.warn('There is no ' + ns_string + ' subscription');
                                                }
                                                return null;
                                        }
                                        nsObject = nsObject[iPart];

                                        if(recurrent === true && typeof depth !== 'number') { //depth is not defined
                                                executeCallback(nsObject._events, args, async);
                                        } else if(recurrent === true && typeof depth === 'number' && i >= partsLength - depth) { //if depth is defined
                                                executeCallback(nsObject._events, args, async);
                                        }
                                }

                                if(recurrent === false) {
                                        executeCallback(nsObject._events, args, async);
                                }
                        },
                        /**
                         * Subscribe event
                         * @param ns_string string namespace string splited by dots
                         * @param callback function function executed after publishing event
                         * @param givenObject object/nothing Optional object which will be used as "this" in callback
                         */
                        subscribe : function(ns_string, callback, givenObject) {
                                var that = this,
                                        subscriptions = [];

                                //if we have array of callbacks - multiple subscribtion
                                if(typeof callback === 'object' && callback instanceof Array) {
                                        forEach(callback, function(number) {
                                                var oneCallback = callback[number];

                                                subscriptions = subscriptions.concat(that.subscribe.apply(that, [ns_string, oneCallback, givenObject]));
                                        });
                                } else if(typeof ns_string === 'object' && ns_string instanceof Array) {
                                        forEach(ns_string, function(number) {
                                                var namespace = ns_string[number];

                                                subscriptions = subscriptions.concat(that.subscribe.apply(that, [namespace, callback, givenObject]));
                                        });
                                } else {
                                        return subscribe.apply(that, arguments);
                                }
                                return subscriptions;
                        },
                        subscribeOnce : function(ns_string, callback, givenObject) {
                                var that = this;
                                var subscribtion = null;
                                var subscribtionCallback = function() {
                                                callback.apply(this, arguments);
                                                that.unsubscribe(subscribtion);
                                        };

                                subscribtion = that.subscribe.apply(that, [ns_string, subscribtionCallback, givenObject]);
                        },
                        unsubscribe : function(subscribeObject) {
                                var that = this;

                                //if we have array of callbacks - multiple subscribtion
                                if(subscribeObject instanceof Array) {
                                        forEach(subscribeObject, function(number) {
                                                var oneSubscribtion = subscribeObject[number];

                                                unsubscribe.apply(that, [oneSubscribtion]);
                                        });
                                } else {
                                        unsubscribe.apply(that, arguments);
                                }
                        },
                        newInstance : function(config) {
                                return new Pubsub(config);
                        }
                }; //return block
        }
        pubsubInstance = new Pubsub(pubsubConfig);

        //if sbd's using requirejs library to load pubsub.js
        if(typeof define === 'function') {
                define(pubsubInstance);
        }

        //node.js
        if(typeof module === 'object' && module.exports) {
                module.exports = pubsubInstance;
        }

        if(window) {
                window.pubsub = pubsubInstance;
        }
        if(window && window !== scope) {
                scope.pubsub = pubsubInstance;
        }
})(this);
</script>
<script type="text/javascript">
  window.iter = 0
  window.callback = function() {
    iter++
  };
  window.payload = {
    somekey: 'some value'
  };




  jQuery(function() {
    jQuery(window).on('app', callback);
    jQuery(window).on('app/region', callback);
    jQuery(window).on('app/region/module', callback);
    jQuery(window).on('app/region/module/event', callback);

    PubSub.subscribe('app', callback);
    PubSub.subscribe('app.region', callback);
    PubSub.subscribe('app.region.module', callback);
    PubSub.subscribe('app.region.module.event', callback);
  
    subtopic.subscribe('app', callback);
    subtopic.subscribe('app/region', callback);
    subtopic.subscribe('app/region/module', callback);
    subtopic.subscribe('app/region/module/event', callback);

    Events.subscribe('app', callback);
    Events.subscribe('app/region', callback);
    Events.subscribe('app/region/module', callback);
    Events.subscribe('app/region/module/event', callback);

    amplify.subscribe('app', callback);
    amplify.subscribe('app/region', callback);
    amplify.subscribe('app/region/module', callback);
    amplify.subscribe('app/region/module/event', callback);

    Spine.bind('app', callback);
    Spine.bind('app/region', callback);
    Spine.bind('app/region/module', callback);
    Spine.bind('app/region/module/event', callback);

    Ply.core.listen('app', callback);
    Ply.core.listen('app/region', callback);
    Ply.core.listen('app/region/module', callback);
    Ply.core.listen('app/region/module/event', callback);
        
          pubsub.subscribe('app', callback);
    pubsub.subscribe('app/region', callback);
    pubsub.subscribe('app/region/module', callback);
    pubsub.subscribe('app/region/module/event', callback);
});
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
jQuery Events
jQuery({})
  .trigger('app', payload)
  .trigger('app/region', payload)
  .trigger('app/region/module', payload)
  .trigger('app/region/module/event', payload);
ready
PubSubJS - async
PubSub.publish('app.region.module.event', payload);
ready
PubSubJS - sync
PubSub.publishSync('app.region.module.event', payload);
ready
Pure JS PubSub
Events.publish('app', [payload]);
Events.publish('app/region', [payload]);
Events.publish('app/region/module', [payload]);
Events.publish('app/region/module/event', [payload]);
ready
Amplify Pub/Sub
amplify.publish('app', payload);
amplify.publish('app/region', payload);
amplify.publish('app/region/module', payload);
amplify.publish('app/region/module/event', payload);
ready
Spine Events
Spine.trigger('app', payload);
Spine.trigger('app/region', payload);
Spine.trigger('app/region/module', payload);
Spine.trigger('app/region/module/event', payload);
ready
Ply Notify/Listen
Ply.core.notify('app', window, payload);
Ply.core.notify('app/region', window, payload);
Ply.core.notify('app/region/module', window, payload);
Ply.core.notify('app/region/module/event', window, payload);
ready
Subtopic
subtopic.publish('app/region/module/event', [payload]);
ready
pubsub
pubsub.subscribe('app', callback);
pubsub.subscribe('app/region', callback);
pubsub.subscribe('app/region/module', callback);
pubsub.subscribe('app/region/module/event', callback);
ready

Revisions

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