...
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 can 152 // 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 noop 167 // 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, or 187 // false if there are no changed attributes. Useful for determining what 188 // parts of a view need to be updated and/or what attributes need to be 189 // 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 last 207 // `"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 previous 214 // `"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's 220 // 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's 237 // 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 as 252 // `set(attr).save(null, opts)` with validation. Otherwise, check if 253 // 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're 321 // using Backbone's restful methods, override this to change the endpoint 322 // 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` on 334 // 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 the 372 // 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');