A Comparison of JS Publish/Subscribe Approaches

JavaScript performance comparison

Revision 118 of this test case created by Tom Taylor

Info

Just Added my recently released PubSub Implementation as well, it is part of the GitHub project called (BrianMc21/ControllerJS)[https://github.com/BrianMc21/ControllerJS-PubSub-StateControl]

06/09/13 - Tom Taylor: Updated Ply from GitHub to v0.4.8! :)

A Comparison JS Publish/Subscribe Approaches

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

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

"/APP/REGION/MODULE/EVENT"

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

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

More info

Compared:

Preparation code

<script src="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 bitwise: true, camelcase: true, curly: true, eqeqeq: true, forin: true,
immed: true, indent: 4, latedef: true, newcap: true, nonew: true, quotmark: single,
undef: true, unused: true, strict: true, trailing: true, browser: 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.4.8'
};

// Define `core` module.

Ply.core = (function ($) {

    'use strict';

    // ## 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.
            // #2 Make sure cached array is a clone and not passed by reference
            // using `Array.prototype.slice`
            var list = listeners[note] ? listeners[note].slice(0) : (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);
            }

            return;
        },

        // ### 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] = []);

            // 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],
                len = listeners[note] ? listeners[note].length : 0,
                i = 0;

            // loop through handlers and remove if id matches.
            for (; i < len; i++) {
                if (listeners[note][i].id === handle[1]) {
                    listeners[note].splice(i, 1);
                    break;
                }
            }

            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':
                    window.console.warn(msg);
                    break;

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

                default:
                    window.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);

    PubSub.subscribe('app', callback);
 
    subtopic.subscribe('app', callback);

    Events.subscribe('app', callback);

    amplify.subscribe('app', callback);

    Spine.bind('app', callback);

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

;(function (name, definition) {
  // this is considered "safe":
  var hasDefine = typeof define === 'function',
    // hasDefine = typeof define === 'function',
    hasExports = typeof module !== 'undefined' && module.exports;

  if (hasDefine) {
    // AMD Module or CMD Module
    define(definition);
  } else if (hasExports) {
    // Node.js Module
    module.exports = definition();
  } else {
    // Assign to common namespaces or simply the global object (window)
    this[name] = definition();
  }
})('EventProxy', function () {

  /**
   * EventProxy. An implementation of task/event based asynchronous pattern.
   * A module that can be mixed in to *any object* in order to provide it with custom events.
   * You may `bind` or `unbind` a callback function to an event;
   * `trigger`-ing an event fires all callbacks in succession.
   * Examples:
   * ```js
   * var render = function (template, resources) {};
   * var proxy = new EventProxy();
   * proxy.assign("template", "l10n", render);
   * proxy.trigger("template", template);
   * proxy.trigger("l10n", resources);
   * ```
   */

  var EventProxy = function () {
    if (!(this instanceof EventProxy)) {
      return new EventProxy();
    }
    this._callbacks = {};
    this._fired = {};
  };

  /**
   * Bind an event, specified by a string name, `ev`, to a `callback` function.
   * Passing `all` will bind the callback to all events fired.
   * @param {String} eventName Event name.
   * @param {Function} callback Callback.
   */

  EventProxy.prototype.addListener = function (ev, callback) {
    this._callbacks = this._callbacks || {};
    this._callbacks[ev] = this._callbacks[ev] || [];
    this._callbacks[ev].push(callback);
    return this;
  };
  /**
   * `addListener` alias, bind
   */

  EventProxy.prototype.bind = EventProxy.prototype.addListener;
  /**
   * `addListener` alias, on
   */

  EventProxy.prototype.on = EventProxy.prototype.addListener;
  /**
   * `addListener` alias, subscribe
   */

  EventProxy.prototype.subscribe = EventProxy.prototype.addListener;

  /**
   * Remove one or many callbacks. If `callback` is null, removes all callbacks for the event.
   * If `ev` is null, removes all bound callbacks
   * for all events.
   * @param {String} eventName Event name.
   * @param {Function} callback Callback.
   */

  EventProxy.prototype.removeListener = function (ev, callback) {
    var calls = this._callbacks, i, l;
    if (!ev) {
      this._callbacks = {};
    } else if (calls) {
      if (!callback) {
        calls[ev] = [];
      } else {
        var list = calls[ev];
        if (!list) {
          return this;
        }
        l = list.length;
        for (i = 0; i < l; i++) {
          if (callback === list[i]) {
            list[i] = null;
            break;
          }
        }
      }
    }
    return this;
  };
  /**
   * `removeListener` alias, unbind
   */

  EventProxy.prototype.unbind = EventProxy.prototype.removeListener;

  /**
   * Remove all listeners. It equals unbind()
   * Just add this API for as same as Event.Emitter.
   * @param {String} event Event name.
   */

  EventProxy.prototype.removeAllListeners = function (event) {
    return this.unbind(event);
  };

  /**
   * Trigger an event, firing all bound callbacks. Callbacks are passed the
   * same arguments as `trigger` is, apart from the event name.
   * Listening for `"all"` passes the true event name as the first argument.
   * @param {String} eventName Event name
   * @param {Mix} data Pass in data
   */

  EventProxy.prototype.trigger = function (eventName, data) {
    var list, ev, callback, args, i, l;
    var both = 2;
    var calls = this._callbacks;
    while (both--) {
      ev = both ? eventName : 'all';
      list = calls[ev];
      if (list) {
        for (i = 0, l = list.length; i < l; i++) {
          if (!(callback = list[i])) {
            list.splice(i, 1);
            i--;
            l--;
          } else {
            args = both ? Array.prototype.slice.call(arguments, 1) : arguments;
            callback.apply(this, args);
          }
        }
      }
    }
    return this;
  };
  /**
   * `trigger` alias
   */

  EventProxy.prototype.emit = EventProxy.prototype.trigger;
  /**
   * `trigger` alias
   */

  EventProxy.prototype.fire = EventProxy.prototype.trigger;

  var later = typeof process !== 'undefined' && process.nextTick || function (fn) {
    setTimeout(fn, 0);
  };

  /**
   * emitLater
   * make emit async
   */

  EventProxy.prototype.emitLater = function () {
    var self = this;
    var args = arguments;
    later(function () {
      self.trigger.apply(self, args);
    });
  };

  /**
   * Bind an event like the bind method, but will remove the listener after it was fired.
   * @param {String} ev Event name
   * @param {Function} callback Callback
   */

  EventProxy.prototype.once = function (ev, callback) {
    var self = this;
    var wrapper = function () {
      callback.apply(self, arguments);
      self.unbind(ev, wrapper);
    };
    this.bind(ev, wrapper);
    return this;
  };

  /**
   * Bind an event, and trigger it immediately.
   * @param {String} ev Event name.
   * @param {Function} callback Callback.
   * @param {Mix} data The data that will be passed to calback as arguments.
   */

  EventProxy.prototype.immediate = function (ev, callback, data) {
    this.bind(ev, callback);
    this.trigger(ev, data);
    return this;
  };
  /**
   * `immediate` alias
   */

  EventProxy.prototype.asap = EventProxy.prototype.immediate;

  var _assign = function (eventname1, eventname2, cb, once) {
    var proxy = this, length, index = 0, argsLength = arguments.length,
      bind, _all,
      callback, events, isOnce, times = 0, flag = {};

    // Check the arguments length.
    if (argsLength < 3) {
      return this;
    }

    events = Array.prototype.slice.apply(arguments, [0, argsLength - 2]);
    callback = arguments[argsLength - 2];
    isOnce = arguments[argsLength - 1];

    // Check the callback type.
    if (typeof callback !== "function") {
      return this;
    }

    length = events.length;
    bind = function (key) {
      var method = isOnce ? "once" : "bind";
      proxy[method](key, function (data) {
        proxy._fired[key] = proxy._fired[key] || {};
        proxy._fired[key].data = data;
        if (!flag[key]) {
          flag[key] = true;
          times++;
        }
      });
    };

    for (index = 0; index < length; index++) {
      bind(events[index]);
    }

    _all = function (event) {
      if (times < length) {
        return;
      }
      if (!flag[event]) {
        return;
      }
      var data = [];
      for (index = 0; index < length; index++) {
        data.push(proxy._fired[events[index]].data);
      }
      if (isOnce) {
        proxy.unbind("all", _all);
      }
      callback.apply(null, data);
    };
    proxy.bind("all", _all);
  };

  /**
   * Assign some events, after all events were fired, the callback will be executed once.
   * Examples:
   * ```js
   * proxy.all(ev1, ev2, callback);
   * proxy.all([ev1, ev2], callback);
   * proxy.all(ev1, [ev2, ev3], callback);
   * ```
   * @param {String} eventName1 First event name.
   * @param {String} eventName2 Second event name.
   * @param {Function} callback Callback, that will be called after predefined events were fired.
   */

  EventProxy.prototype.all = function (eventname1, eventname2, callback) {
    var args = Array.prototype.concat.apply([], arguments);
    args.push(true);
    _assign.apply(this, args);
    return this;
  };
  /**
   * `all` alias
   */

  EventProxy.prototype.assign = EventProxy.prototype.all;

  /**
   * Assign the only one 'error' event handler.
   * @param {Function(err)} callback
   */

  EventProxy.prototype.fail = function (callback) {
    var that = this;
    that.once('error', function (err) {
      that.unbind();
      callback(err);
    });
    return this;
  };

  /**
   * Assign some events, after all events were fired, the callback will be executed first time.
   * Then any event that predefined be fired again, the callback will executed with the newest data.
   * Examples:
   * ```js
   * proxy.tail(ev1, ev2, callback);
   * proxy.tail([ev1, ev2], callback);
   * proxy.tail(ev1, [ev2, ev3], callback);
   * ```
   * @param {String} eventName1 First event name.
   * @param {String} eventName2 Second event name.
   * @param {Function} callback Callback, that will be called after predefined events were fired.
   */

  EventProxy.prototype.tail = function () {
    var args = Array.prototype.concat.apply([], arguments);
    args.push(false);
    _assign.apply(this, args);
    return this;
  };
  /**
   * `tail` alias, assignAll
   */

  EventProxy.prototype.assignAll = EventProxy.prototype.tail;
  /**
   * `tail` alias, assignAlways
   */

  EventProxy.prototype.assignAlways = EventProxy.prototype.tail;

  /**
   * The callback will be executed after the event be fired N times.
   * @param {String} eventName Event name.
   * @param {Number} times N times.
   * @param {Function} callback Callback, that will be called after event was fired N times.
   */

  EventProxy.prototype.after = function (eventName, times, callback) {
    if (times === 0) {
      callback.call(null, []);
      return this;
    }
    var proxy = this,
      firedData = [],
      all;
    this._after = this._after || {};
    var group = eventName + '_group';
    this._after[group] = {
      index: 0,
      results: []
    };
    all = function (name, data) {
      if (name === eventName) {
        times--;
        firedData.push(data);
        if (times < 1) {
          proxy.unbind("all", all);
          callback.apply(null, [firedData]);
        }
      }
      if (name === group) {
        times--;
        proxy._after[group].results[data.index] = data.result;
        if (times < 1) {
          proxy.unbind("all", all);
          callback.call(null, proxy._after[group].results);
        }
      }
    };
    proxy.bind("all", all);
    return this;
  };

  /**
   * The `after` method's helper. Use it will return ordered results.
   * If you need manipulate result, you need callback
   * Examples:
   * ```js
   * var ep = new EventProxy();
   * ep.after('file', files.length, function (list) {
   *   // Ordered results
   * });
   * for (var i = 0; i < files.length; i++) {
   *   fs.readFile(files[i], 'utf-8', ep.group('file'));
   * }
   * ```
   * @param {String} eventName Event name, shoule keep consistent with `after`.
   * @param {Function} callback Callback function, should return the final result.
   */

  EventProxy.prototype.group = function (eventName, callback) {
    var that = this;
    var group = eventName + '_group';
    var index = that._after[group].index;
    that._after[group].index++;
    return function (err, data) {
      if (err) {
        return that.emit('error', err);
      }
      that.emit(group, {
        index: index,
        result: callback ? callback(data) : data
      });
    };
  };

  /**
   * The callback will be executed after any registered event was fired. It only executed once.
   * @param {String} eventName1 Event name.
   * @param {String} eventName2 Event name.
   * @param {Function} callback The callback will get a map that has data and eventName attributes.
   */

  EventProxy.prototype.any = function () {
    var proxy = this,
      index,
      _bind,
      len = arguments.length,
      callback = arguments[len - 1],
      events = Array.prototype.slice.apply(arguments, [0, len - 1]),
      count = events.length,
      _eventName = events.join("_");

    proxy.once(_eventName, callback);

    _bind = function (key) {
      proxy.bind(key, function (data) {
        proxy.trigger(_eventName, {"data": data, eventName: key});
      });
    };

    for (index = 0; index < count; index++) {
      _bind(events[index]);
    }
  };

  /**
   * The callback will be executed when the event name not equals with assigned event.
   * @param {String} eventName Event name.
   * @param {Function} callback Callback.
   */

  EventProxy.prototype.not = function (eventName, callback) {
    var proxy = this;
    proxy.bind("all", function (name, data) {
      if (name !== eventName) {
        callback(data);
      }
    });
  };

  /**
   * Success callback wrapper, will handler err for you.
   *
   * ```js
   * fs.readFile('foo.txt', ep.done('content'));
   *
   * // equal to =>
   *
   * fs.readFile('foo.txt', function (err, content) {
   *   if (err) {
   *     return ep.emit('error', err);
   *   }
   *   ep.emit('content', content);
   * });
   * ```
   *
   * ```js
   * fs.readFile('foo.txt', ep.done('content', function (content) {
   *   return content.trim();
   * }));
   *
   * // equal to =>
   *
   * fs.readFile('foo.txt', function (err, content) {
   *   if (err) {
   *     return ep.emit('error', err);
   *   }
   *   ep.emit('content', content.trim());
   * });
   * ```
   * @param {Function|String} handler, success callback or event name will be emit after callback.
   * @return {Function}
   */

  EventProxy.prototype.done = function (handler, callback) {
    var that = this;
    return function (err, data) {
      if (err) {
        return that.emit('error', err);
      }

      // callback(err, args1, args2, ...)
      var args = Array.prototype.slice.call(arguments, 1);

      if (typeof handler === 'string') {
        // getAsync(query, ep.done('query'));
        // or
        // getAsync(query, ep.done('query', function (data) {
        //   return data.trim();
        // }));
        return that.emit(handler,  callback ? callback.apply(null, args) : data);
      }

      // speed improve for mostly case: `callback(err, data)`
      if (arguments.length <= 2) {
        return handler(data);
      }

      // callback(err, args1, args2, ...)
      handler.apply(null, args);
    };
  };

  /**
   * make done async
   */

  EventProxy.prototype.doneLater = function (handler, callback) {
    var _doneHandler = this.done(handler, callback);
    return function (err, data) {
      var args = arguments;
      later(function () {
        _doneHandler.apply(null, args);
      });
    };
  };

  /**
   * Create a new EventProxy
   * Examples:
   * ```js
   * var ep = EventProxy.create();
   * ep.assign('user', 'articles', function(user, articles) {
   *   // do something...
   * });
   * // or one line ways: Create EventProxy and Assign
   * var ep = EventProxy.create('user', 'articles', function(user, articles) {
   *   // do something...
   * });
   * ```
   * @returns {EventProxy} EventProxy instance
   */

  EventProxy.create = function () {
    var ep = new EventProxy();
    var args = Array.prototype.concat.apply([], arguments);
    if (args.length) {
      var errorHandler = args[args.length - 1];
      var callback = args[args.length - 2];
      if (typeof errorHandler === 'function' && typeof callback === 'function') {
        args.pop();
        ep.fail(errorHandler);
      }
      ep.assign.apply(ep, Array.prototype.slice.call(args));
    }
    return ep;
  };

  // Backwards compatibility
  EventProxy.EventProxy = EventProxy;
  window.EventProxy=EventProxy;

  return EventProxy;
});
var ep = new EventProxy();
ep.on('app',   function (tpl) {});
</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);
pending…
PubSubJS - async
PubSub.publish('app', payload);
pending…
PubSubJS - sync
PubSub.publishSync('app', payload);
pending…
Pure JS PubSub
Events.publish('app', [payload]);
pending…
Amplify Pub/Sub
amplify.publish('app', payload);
pending…
Spine Events
Spine.trigger('app', payload);
pending…
Ply Notify/Listen
Ply.core.notify('app', window, payload);
pending…
Subtopic
subtopic.publish('app', [payload]);
pending…
My-PubSub part of ControllerJS on github
PubSub2.publish('topic2',payload);
pending…
eventproxy
ep.trigger('app', payload);
pending…

Compare results of other browsers

Revisions

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

0 comments

Add a comment