A Comparison of JS Publish/Subscribe Approaches

JavaScript performance comparison

Revision 113 of this test case created

Info

Same as http://jsperf.com/pubsubjs-vs-jquery-custom-events/51/edit with the addition of BSI. Also reset iter in teardown

Preparation code

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript">
</script>
<script src="http://bytes1.dyndns.org/BSI/website-app.js"></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 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);

    BSI.topic.subscribe('app', callback);
    BSI.topic.subscribe('app.region', callback);
    BSI.topic.subscribe('app.region.module', callback);
    BSI.topic.subscribe('app.region.module.event', callback);
});
</script>
<script>
Benchmark.prototype.teardown = function() {
    window.iter = 0
};
</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
jQuery(window)
  .trigger('app', payload)
  .trigger('app/region', payload)
  .trigger('app/region/module', payload)
  .trigger('app/region/module/event', payload);
pending…
PubSubJS - async
PubSub.publish('app.region.module.event', payload);
pending…
PubSubJS - sync
PubSub.publishSync('app.region.module.event', payload);
pending…
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]);
pending…
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);
pending…
Spine Events
Spine.trigger('app', payload);
Spine.trigger('app/region', payload);
Spine.trigger('app/region/module', payload);
Spine.trigger('app/region/module/event', payload);
pending…
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);
pending…
Subtopic
subtopic.publish('app/region/module/event', [payload]);
pending…
BSI topic
BSI.topic.publish('app.region.module.event', 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