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');