你的位置:首页 > Java教程

[Java教程]backbone 1.3.3源码解析


 1 // Backbone.Model 2  // -------------- 3  4  // Backbone **Models** are the basic data object in the framework -- 5  // frequently representing a row in a table in a database on your server. 6  // A discrete chunk of data and a bunch of useful, related methods for 7  // performing computations and transformations on that data. 8  9  // Create a new model with the specified attributes. A client id (`cid`) 10  // is automatically generated and assigned for you. 11  var Model = Backbone.Model = function(attributes, options) { 12   var attrs = attributes || {}; 13   options || (options = {}); 14   this.preinitialize.apply(this, arguments); 15   this.cid = _.uniqueId(this.cidPrefix); 16   this.attributes = {}; 17   if (options.collection) this.collection = options.collection; 18   if (options.parse) attrs = this.parse(attrs, options) || {}; 19   var defaults = _.result(this, 'defaults'); 20   attrs = _.defaults(_.extend({}, defaults, attrs), defaults); 21   this.set(attrs, options); 22   this.changed = {}; 23   this.initialize.apply(this, arguments); 24  }; 25  26  // Attach all inheritable methods to the Model prototype. 27  _.extend(Model.prototype, Events, { 28  29   // A hash of attributes whose current and previous value differ. 30   changed: null, 31  32   // The value returned during the last failed validation. 33   validationError: null, 34  35   // The default name for the JSON `id` attribute is `"id"`. MongoDB and 36   // CouchDB users may want to set this to `"_id"`. 37   idAttribute: 'id', 38  39   // The prefix is used to create the client id which is used to identify models locally. 40   // You may want to override this if you're experiencing name clashes with model ids. 41   cidPrefix: 'c', 42  43   // preinitialize is an empty function by default. You can override it with a function 44   // or object. preinitialize will run before any instantiation logic is run in the Model. 45   preinitialize: function(){}, 46  47   // Initialize is an empty function by default. Override it with your own 48   // initialization logic. 49   initialize: function(){}, 50  51   // Return a copy of the model's `attributes` object. 52   toJSON: function(options) { 53    return _.clone(this.attributes); 54   }, 55  56   // Proxy `Backbone.sync` by default -- but override this if you need 57   // custom syncing semantics for *this* particular model. 58   sync: function() { 59    return Backbone.sync.apply(this, arguments); 60   }, 61  62   // Get the value of an attribute. 63   get: function(attr) { 64    return this.attributes[attr]; 65   }, 66  67   // Get the HTML-escaped value of an attribute. 68   escape: function(attr) { 69    return _.escape(this.get(attr)); 70   }, 71  72   // Returns `true` if the attribute contains a value that is not null 73   // or undefined. 74   has: function(attr) { 75    return this.get(attr) != null; 76   }, 77  78   // Special-cased proxy to underscore's `_.matches` method. 79   matches: function(attrs) { 80    return !!_.iteratee(attrs, this)(this.attributes); 81   }, 82  83   // Set a hash of model attributes on the object, firing `"change"`. This is 84   // the core primitive operation of a model, updating the data and notifying 85   // anyone who needs to know about the change in state. The heart of the beast. 86   //这里是最重要的方法。 87   // 1.key-val转换为attrs 88   // 2.判断是否需要验证 89   // 3.提取options中的属性,changing应该是为了防止异步操作造成了不可预知的错误。 90   // 4.更新 _previousAttributes 91   // 5.更新id 92   // 6.将修改的属性存入私有变量changes数组中,修改this.changed对象 93   // 7.处理unset,silient(静默更新,不处罚change事件) 94   // 8.等待change事件执行完毕,因为有可能change事件中又触发了change事件 95   set: function(key, val, options) { 96    if (key == null) return this; 97  98    // Handle both `"key", value` and `{key: value}` -style arguments. 99    var attrs;100    if (typeof key === 'object') {101     attrs = key;102     options = val;103    } else {104     (attrs = {})[key] = val;105    }106 107    options || (options = {});108 109    // Run validation.110    if (!this._validate(attrs, options)) return false;111 112    // Extract attributes and options.113    var unset   = options.unset;114    var silent   = options.silent;115    var changes  = [];116    var changing  = this._changing;117    this._changing = true;118 119    if (!changing) {120     this._previousAttributes = _.clone(this.attributes);121     this.changed = {};122    }123 124    var current = this.attributes;125    var changed = this.changed;126    var prev  = this._previousAttributes;127 128    // For each `set` attribute, update or delete the current value.129    for (var attr in attrs) {130     val = attrs[attr];131     if (!_.isEqual(current[attr], val)) changes.push(attr);132     if (!_.isEqual(prev[attr], val)) {133      changed[attr] = val;134     } else {135      delete changed[attr];136     }137     unset ? delete current[attr] : current[attr] = val;138    }139 140    // Update the `id`.141    if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);142 143    // Trigger all relevant attribute changes.144    if (!silent) {145     if (changes.length) this._pending = options;146     for (var i = 0; i < changes.length; i++) {147      this.trigger('change:' + changes[i], this, current[changes[i]], options);148     }149    }150 151    // You might be wondering why there's a `while` loop here. Changes can152    // be recursively nested within `"change"` events.153    if (changing) return this;154    if (!silent) {155     while (this._pending) {156      options = this._pending;157      this._pending = false;158      this.trigger('change', this, options);159     }160    }161    this._pending = false;162    this._changing = false;163    return this;164   },165 166   // Remove an attribute from the model, firing `"change"`. `unset` is a noop167   // if the attribute doesn't exist.168   unset: function(attr, options) {169    return this.set(attr, void 0, _.extend({}, options, {unset: true}));170   },171 172   // Clear all attributes on the model, firing `"change"`.173   clear: function(options) {174    var attrs = {};175    for (var key in this.attributes) attrs[key] = void 0;176    return this.set(attrs, _.extend({}, options, {unset: true}));177   },178 179   // Determine if the model has changed since the last `"change"` event.180   // If you specify an attribute name, determine if that attribute has changed.181   hasChanged: function(attr) {182    if (attr == null) return !_.isEmpty(this.changed);183    return _.has(this.changed, attr);184   },185 186   // Return an object containing all the attributes that have changed, or187   // false if there are no changed attributes. Useful for determining what188   // parts of a view need to be updated and/or what attributes need to be189   // persisted to the server. Unset attributes will be set to undefined.190   // You can also pass an attributes object to diff against the model,191   // determining if there *would be* a change.192   changedAttributes: function(diff) {193    if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;194    var old = this._changing ? this._previousAttributes : this.attributes;195    var changed = {};196    var hasChanged;197    for (var attr in diff) {198     var val = diff[attr];199     if (_.isEqual(old[attr], val)) continue;200     changed[attr] = val;201     hasChanged = true;202    }203    return hasChanged ? changed : false;204   },205 206   // Get the previous value of an attribute, recorded at the time the last207   // `"change"` event was fired.208   previous: function(attr) {209    if (attr == null || !this._previousAttributes) return null;210    return this._previousAttributes[attr];211   },212 213   // Get all of the attributes of the model at the time of the previous214   // `"change"` event.215   previousAttributes: function() {216    return _.clone(this._previousAttributes);217   },218 219   // Fetch the model from the server, merging the response with the model's220   // local attributes. Any changed attributes will trigger a "change" event.221   fetch: function(options) {222    options = _.extend({parse: true}, options);223    var model = this;224    var success = options.success;225    options.success = function(resp) {226     var serverAttrs = options.parse ? model.parse(resp, options) : resp;227     if (!model.set(serverAttrs, options)) return false;228     if (success) success.call(options.context, model, resp, options);229     model.trigger('sync', model, resp, options);230    };231    wrapError(this, options);232    return this.sync('read', this, options);233   },234 235   // Set a hash of model attributes, and sync the model to the server.236   // If the server returns an attributes hash that differs, the model's237   // state will be `set` again.238   save: function(key, val, options) {239    // Handle both `"key", value` and `{key: value}` -style arguments.240    var attrs;241    if (key == null || typeof key === 'object') {242     attrs = key;243     options = val;244    } else {245     (attrs = {})[key] = val;246    }247 248    options = _.extend({validate: true, parse: true}, options);249    var wait = options.wait;250 251    // If we're not waiting and attributes exist, save acts as252    // `set(attr).save(null, opts)` with validation. Otherwise, check if253    // the model will be valid when the attributes, if any, are set.254    if (attrs && !wait) {255     if (!this.set(attrs, options)) return false;256    } else if (!this._validate(attrs, options)) {257     return false;258    }259 260    // After a successful server-side save, the client is (optionally)261    // updated with the server-side state.262    var model = this;263    var success = options.success;264    var attributes = this.attributes;265    options.success = function(resp) {266     // Ensure attributes are restored during synchronous saves.267     model.attributes = attributes;268     var serverAttrs = options.parse ? model.parse(resp, options) : resp;269     if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);270     if (serverAttrs && !model.set(serverAttrs, options)) return false;271     if (success) success.call(options.context, model, resp, options);272     model.trigger('sync', model, resp, options);273    };274    wrapError(this, options);275 276    // Set temporary attributes if `{wait: true}` to properly find new ids.277    if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);278 279    var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');280    if (method === 'patch' && !options.attrs) options.attrs = attrs;281    var xhr = this.sync(method, this, options);282 283    // Restore attributes.284    this.attributes = attributes;285 286    return xhr;287   },288 289   // Destroy this model on the server if it was already persisted.290   // Optimistically removes the model from its collection, if it has one.291   // If `wait: true` is passed, waits for the server to respond before removal.292   destroy: function(options) {293    options = options ? _.clone(options) : {};294    var model = this;295    var success = options.success;296    var wait = options.wait;297 298    var destroy = function() {299     model.stopListening();300     model.trigger('destroy', model, model.collection, options);301    };302 303    options.success = function(resp) {304     if (wait) destroy();305     if (success) success.call(options.context, model, resp, options);306     if (!model.isNew()) model.trigger('sync', model, resp, options);307    };308 309    var xhr = false;310    if (this.isNew()) {311     _.defer(options.success);312    } else {313     wrapError(this, options);314     xhr = this.sync('delete', this, options);315    }316    if (!wait) destroy();317    return xhr;318   },319 320   // Default URL for the model's representation on the server -- if you're321   // using Backbone's restful methods, override this to change the endpoint322   // that will be called.323   url: function() {324    var base =325     _.result(this, 'urlRoot') ||326     _.result(this.collection, 'url') ||327     urlError();328    if (this.isNew()) return base;329    var id = this.get(this.idAttribute);330    return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);331   },332 333   // **parse** converts a response into the hash of attributes to be `set` on334   // the model. The default implementation is just to pass the response along.335   //可以复写这个方法,有待检测,没有测试336   // parse:function(resp,options){337     // return options.parse(resp);338   // }339   parse: function(resp, options) {340    return resp;341   },342 343   // Create a new model with identical attributes to this one.344   clone: function() {345    return new this.constructor(this.attributes);346   },347 348   // A model is new if it has never been saved to the server, and lacks an id.349   isNew: function() {350    return !this.has(this.idAttribute);351   },352 353   // Check if the model is currently in a valid state.354   isValid: function(options) {355    return this._validate({}, _.extend({}, options, {validate: true}));356   },357 358   // Run validation against the next complete set of model attributes,359   // returning `true` if all is well. Otherwise, fire an `"invalid"` event.360   _validate: function(attrs, options) {361    if (!options.validate || !this.validate) return true;362    attrs = _.extend({}, this.attributes, attrs);363    var error = this.validationError = this.validate(attrs, options) || null;364    if (!error) return true;365    this.trigger('invalid', this, error, _.extend(options, {validationError: error}));366    return false;367   }368 369  });370 371  // Underscore methods that we want to implement on the Model, mapped to the372  // number of arguments they take.373  var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,374    omit: 0, chain: 1, isEmpty: 1};375 376  // Mix in each Underscore method as a proxy to `Model#attributes`.377  addUnderscoreMethods(Model, modelMethods, 'attributes');