首页 > 技术文章 > Backbone事件机制核心源码(仅包含Events、Model模块)

hellohuman 2014-09-07 08:50 原文

一、应用场景
为了改善酷版139邮箱的代码结构,引入backbone的事件机制,按照MVC的分层思想搭建酷版云邮局的代码框架。力求在保持酷版轻量级的基础上提高代码的可维护性。
 
二、遗留问题
1、backbone的升级问题,新的特性无法引入
2、backbone中的潜在BUG,若官方已修复则无法同步更新
解决办法:
关注backbone官网的更新记录。
http://github.com/documentcloud/backbone/
 
三、核心源码 
/**
 * @裁剪版backbone,仅包含Events、Model模块,适用于轻量级的移动终端APP
 * window.Minibone
 */
(function(){
  var root = this;
  
  var array = [];
  var push = array.push;
  var slice = array.slice;
  var splice = array.splice;
  
  var ArrayProto = Array.prototype;
  var nativeIsArray = Array.isArray;
  var nativeForEach = ArrayProto.forEach;
  
  var ObjProto = Object.prototype;
  var toString = ObjProto.toString;
  var hasOwnProperty   = ObjProto.hasOwnProperty;
  
  var Minibone = root.Minibone = {};
  var _ = root._ = {};
  
  var breaker = {};
  
  _.clone = function(obj) {
    if (!_.isObject(obj)) return obj;
    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
  };
  
  // Fill in a given object with default properties.
  _.defaults = function(obj) {
    _.each(slice.call(arguments, 1), function(source) {
      for (var prop in source) {
        if (obj[prop] == null) obj[prop] = source[prop];
      }
    });
    return obj;
  };
  
  _.isArray = nativeIsArray || function(obj) {
    return toString.call(obj) == '[object Array]';
  };
  
  // Internal recursive comparison function.
  function eq(a, b, stack) {
   if(typeof a === 'object' || typeof b === 'object'){
   throw new Error('Function eq only support basic data types:number,string,boolean');
   }
    var className = toString.call(a);
    if (className != toString.call(b)) return false;
    switch (className) {
      case '[object String]':
        return a == String(b);
      case '[object Number]':
        return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
      case '[object Boolean]':
        return +a == +b;
    }
    return true;
  }
  
  // only support basic data types:number,string,boolean
  _.isEqual = function(a, b) {
    return eq(a, b, []);
  };
  
  _.isEmpty = function(obj) {
    if (obj == null) return true;
    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
    for (var key in obj) if (_.has(obj, key)) return false;
    return true;
  };
 
  _.isObject = function(obj) {
    return obj === Object(obj);
  };
  
  _.isFunction = function(obj) {
    return toString.call(obj) == '[object Function]';
  };
  
  _.isString = function(obj) {
    return toString.call(obj) == '[object String]';
  };
  
  _.result = function(object, property) {
    if (object == null) return null;
    var value = object[property];
    return _.isFunction(value) ? value.call(object) : value;
  };
  
  _.has = function(obj, key) {
    return hasOwnProperty.call(obj, key);
  };
  
  // The cornerstone, an `each` implementation, aka `forEach`.
  // Handles objects with the built-in `forEach`, arrays, and raw objects.
  // Delegates to **ECMAScript 5**'s native `forEach` if available.
  _.each = _.forEach = function(obj, iterator, context) {
    if (obj == null) return;
    if (nativeForEach && obj.forEach === nativeForEach) {
      obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
      for (var i = 0, l = obj.length; i < l; i++) {
        if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      for (var key in obj) {
        if (_.has(obj, key)) {
          if (iterator.call(context, obj[key], key, obj) === breaker) return;
        }
      }
    }
  };
  
  _.extend = function(obj) {
    _.each(slice.call(arguments, 1), function(source) {
      for (var prop in source) {
        obj[prop] = source[prop];
      }
    });
    return obj;
  };
  
  var idCounter = 0;
  _.uniqueId = function(prefix) {
    var id = idCounter++;
    return prefix ? prefix + id : id;
  };
 
  // Minibone.Events
  var triggerEvents = function(obj, events, args) {
    var ev, i = -1, l = events.length;
    switch (args.length) {
    case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
    return;
    case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
    return;
    case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
    return;
    case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
    return;
    default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
    }
  };
 
  var Events = Minibone.Events = {
    on: function(name, callback, context) {
      this._events || (this._events = {});
      var list = this._events[name] || (this._events[name] = []);
      list.push({callback: callback, context: context, ctx: context || this});
      return this;
    },
 
    off: function(name, callback, context) {
      var list, ev, events, names, i, l, j, k;
      if (!this._events) return this;
      if (!name && !callback && !context) {
        this._events = {};
        return this;
      }
 
      names = name ? [name] : _.keys(this._events);
      for (i = 0, l = names.length; i < l; i++) {
        name = names[i];
        if (list = this._events[name]) {
          events = [];
          if (callback || context) {
            for (j = 0, k = list.length; j < k; j++) {
              ev = list[j];
              if ((callback && callback !== (ev.callback._callback || ev.callback)) ||
                  (context && context !== ev.context)) {
                events.push(ev);
              }
            }
          }
          this._events[name] = events;
        }
      }
 
      return this;
    },
 
    trigger: function(name) {
      if (!this._events) return this;
      var args = slice.call(arguments, 1);
      var events = this._events[name];
      var allEvents = this._events.all;
      if (events) triggerEvents(this, events, args);
      if (allEvents) triggerEvents(this, allEvents, arguments);
      return this;
    }
  };
 
  _.extend(Minibone, Events);
 
  // Minibone.Model
  var Model = Minibone.Model = function(attributes, options) {
    var defaults;
    var attrs = attributes || {};
    this.cid = _.uniqueId('c');
    this.changed = {};
    this.attributes = {};
    this._changes = [];
    if (options && options.collection) this.collection = options.collection;
    if (options && options.parse) attrs = this.parse(attrs);
    if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults);
    this.set(attrs, {silent: true});
    this._currentAttributes = _.clone(this.attributes);
    this._previousAttributes = _.clone(this.attributes);
    this.initialize.apply(this, arguments);
  };
 
  _.extend(Model.prototype, Events, {
 
    changed: null,
    idAttribute: 'id',
    initialize: function(){},
 
    get: function(attr) {
      return this.attributes[attr];
    },
 
    // Set a hash of model attributes on the object, firing `"change"`. This is
    // the core primitive operation of a model, updating the data and notifying
    // anyone who needs to know about the change in state. The heart of the beast.
    set: function(key, val, options) {
      var attr, attrs, unset, changes, silent, changing, prev, current;
      if (key == null) return this;
 
      // Handle both `"key", value` and `{key: value}` -style arguments.
      if (typeof key === 'object') {
        attrs = key;
        options = val;
      } else {
        (attrs = {})[key] = val;
      }
 
      options || (options = {});
      if (!this._validate(attrs, options)) return false;
      unset           = options.unset;
      silent          = options.silent;
      changes         = [];
      changing        = this._changing;
      this._changing  = true;
 
      if (!changing) {
        this._previousAttributes = _.clone(this.attributes);
        this.changed = {};
      }
      current = this.attributes, prev = this._previousAttributes;
 
      // Check for changes of `id`.
      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
 
      // For each `set` attribute, update or delete the current value.
      for (attr in attrs) {
        val = attrs[attr];
        if (!_.isEqual(current[attr], val)) changes.push(attr);
        if (!_.isEqual(prev[attr], val)) {
          this.changed[attr] = val;
        } else {
          delete this.changed[attr];
        }
        unset ? delete current[attr] : current[attr] = val;
      }
 
      // Trigger all relevant attribute changes.
      if (!silent) {
        if (changes.length) this._pending = true;
        for (var i = 0, l = changes.length; i < l; i++) {
          this.trigger('change:' + changes[i], this, current[changes[i]], options);
        }
      }
 
      if (changing) return this;
      if (!silent) {
        while (this._pending) {
          this._pending = false;
          this.trigger('change', this, options);
        }
      }
      this._pending = false;
      this._changing = false;
      return this;
    },
 
    _validate: function(attrs, options) {
      if (!options.validate || !this.validate) return true;
      attrs = _.extend({}, this.attributes, attrs);
      var error = this.validationError = this.validate(attrs, options) || null;
      if (!error) return true;
      this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
      return false;
    }
 
  });
 
  // Helpers
  var extend = function(protoProps, staticProps) {
    var parent = this;
    var child;
 
    if (protoProps && _.has(protoProps, 'constructor')) {
      child = protoProps.constructor;
    } else {
      child = function(){ parent.apply(this, arguments); };
    }
 
    _.extend(child, parent, staticProps);
 
    var Surrogate = function(){ this.constructor = child; };
    Surrogate.prototype = parent.prototype;
    child.prototype = new Surrogate;
 
    if (protoProps) _.extend(child.prototype, protoProps);
    child.__super__ = parent.prototype;
    
    return child;
  };
 
  Model.extend = extend;
}).call(this);

 

 

推荐阅读