上一篇文章中我们分析了Backbone中的Events模块,这一篇文章我们将对Backbone中非常重要的管理数据的Model模块进行分析。
Backbone中用于处理数据的是Model模块,Model中对数据进行存储是放在Model实例的attributes属性对象里面,Model实例具备对数据进行get、set、clear、destroy、isValid等基本的操作,同时还能跟服务器进行数据传递与获取,以方便对数据进行管理。
新建的模型实例对象默认不具有id属性,model的集合Collection则通过model的id作为标志对model进行管理。
同时,Backbone还对model做了一些underscore的方法的扩展,如获取values等,其实就是通过underscore的对应API对model存数据的attributes属性(是一个对象)进行操作。
下面是添加了详细注释的源代码。
// Backbone.Model
// --------------
// Backbone的 **models**(模型) 在框架中是基本的数据对象 -- 通常代表表格中的一行或者服务器中的一个数据库。用一个离散的数据块和一堆有用的、相关的方法来对这些数据执行计算和转换。
// 用指定的属性(attributes)来创建一个新的模型。一个客户id(`cid`)被自动生成并分配给你的模型。
var Model = Backbone.Model = function(attributes, options) {// 每次new一个model实例的时候会执行此函数
var defaults;
var attrs = attributes || {};// 将attributes(若已传入,否则把一个空对象)赋给attrs
options || (options = {});// 如此处理为了防止报错
this.cid = _.uniqueId('c');// 为该model分配一个cid
this.attributes = {};// 存放数据键值对,用于get(取)、set(设置)数据
_.extend(this, _.pick(options, modelOptions));// 取出modelOptions中指定的属性组成的对象并扩展给nodel实例
if (options.parse) attrs = this.parse(attrs, options) || {};// 如果options有parse方法则执行模型的parse方法(用途??)
if (defaults = _.result(this, 'defaults')) {// 如果'defaults'属性存在,其属性值是函数则返回执行结果,否则原样返回。给模型设置一些默认值可以用此方法
attrs = _.defaults({}, attrs, defaults);// 将从第二个参数开始往后所有参数的属性值都拷贝给第一个参数对象并返回
}
this.set(attrs, options);// 将attrs设置为model实例对象
this.changed = {};// 记录被改变过的model值
this.initialize.apply(this, arguments);// 执行初始化(这一点要特别记住,实例化model对象的时候会自动执行initialize)
};
// A list of options to be attached directly to the model, if provided.
// 如果给Model扩展的option选项里提供了这三个字段则扩展给Model实例提供默认配置
var modelOptions = ['url', 'urlRoot', 'collection'];
// Attach all inheritable methods to the Model prototype.
// 扩展可继承的属性给Model原型以提供通用方法和属性
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
// 记录每次调用set方法时, 被改变数据的key集合(在new一个Model实例的时候初始化为一个hash)
changed: null,
// The value returned during the last failed validation.
// 记录调用Model实例的validate方法返回的错误结果值,全部验证成功则置为null
validationError: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
// 每个模型的唯一标识属性(默认为"id", 通过修改idAttribute可自定义id属性名)
// 如果在设置数据时包含了id属性, 则id将会覆盖模型的id
// id用于在Collection集合中查找和标识模型, 与后台接口通信时也会以id作为一条记录的标识
idAttribute: 'id',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
// new一个Model实例的时候会自动执行该函数,用户可以自己在初始化函数中实现自己的逻辑
initialize: function(){},
// Return a copy of the model's `attributes` object.
// 返回当前模型中数据的一个副本(JSON对象格式)
toJSON: function(options) {
return _.clone(this.attributes);
},
// Proxy `Backbone.sync` by default -- but override this if you need
// custom syncing semantics for *this* particular model.
// 将Model实例的数据同步到服务器的方法,默认使用Backbone的sync方法,你可以针对特定model定制内部实现来重写该方法
sync: function() {
return Backbone.sync.apply(this, arguments);
},
// Get the value of an attribute.
// 获取model实例中的某个属性的值
get: function(attr) {
return this.attributes[attr];
},
// Get the HTML-escaped value of an attribute.
// 根据attr属性名, 获取模型中的数据值, 数据值包含的HTML特殊字符将被转换为HTML实体, 包含 & < > " ' \
// 通过 _.escape方法实现
escape: function(attr) {
return _.escape(this.get(attr));
},
// Returns `true` if the attribute contains a value that is not null
// or undefined.
// 检查模型中是否存在某个属性, 若某属性的值为null或undefined则认为是不存在
has: function(attr) {
return this.get(attr) != null;
},
// 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.
// Model的关键实例方法,将一个hash对象设置为model实例的值,默认会触发"change"事件,设置值后会更新model的数据,若监听了某属性的改变,则可以通知订阅者
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
// 若key未传入则直接返回
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
// 处理"key", value以及{key: value}两种传参方式,统一修正为hash模式来处理
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
// 防止报错
options || (options = {});
// Run validation.执行验证逻辑,若验证失败则直接返回false,不设置为model数据
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
// 抽取出一些属性留到后面用
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
// 重新设置_changing为true,表明正在设置数据
this._changing = true;
// 后面触发change命名空间下的事件或change事件的时候set方法也可能在事件处理函数中被调用。此时的set方法里不会再次覆盖_previousAttributes和changed属性以保证正确性。
if (!changing) {
// 记录被改变之前的值
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
// current记录当前属性对象,prev记录set之前的数据对象拷贝值
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`.若传入了id属性对应的值,则覆盖当前id值
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// For each `set` attribute, update or delete the current value.
// 遍历属性值,执行真正的set操作
for (attr in attrs) {
val = attrs[attr];
// changes记录和当前数据对象对比发生改变的属性名
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
// model实例的changed属性记录跟set之前数据对象相比发生改变的属性和值
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
// unset则直接删除属性值
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
// 触发监听change的事件
if (!silent) {
// 跟当前数据对象相比若有值发生了该变则设置标志位pending状态。触发的change命名空间下的事件时以表明正处于set的pending状态
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);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
// 如果set也正在其他地方执行则返回
if (changing) return this;
// 未设置silent为true则默认会触发change事件
if (!silent) {
while (this._pending) {
this._pending = false;
// change事件的处理函数中若也有set方法则需要再次触发change事件
this.trigger('change', this, options);
}
}
// 初始化辅助变量值
this._pending = false;
this._changing = false;
return this;
},
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
// if the attribute doesn't exist.
// 通过在options中设置unset为true从model中删除某个属性,并触发change事件
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
// Clear all attributes on the model, firing `"change"`.
// 清空model中的数据,并触发change事件
clear: function(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, _.extend({}, options, {unset: true}));
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
// 判定最近的一次change事件触发后model实例数据是否发生改变。如果参数指定了一个属性名,那么判断特定属性名是否发生改变,否则任何属性发生改变都返回true
/**
* 一般在change事件中配合previous或previousAttributes方法使用, 如:
* if(model.hasChanged('attr')) {
* var attrPrev = model.previous('attr');
* }
*/
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
// 返回包含所有发生了改变的属性对象,如果没有任何属性值发生改变则返回false。这个方法对以下操作会很有用:
// 1、只把发生了改变的数据上传服务器;2、把发生改变的数据渲染到View
// 你也可以传入一个hash对象,返回该对象参数与model实例不一样的属性值
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
// 在模型触发的change事件中, 获取某个属性被改变前上一个状态的数据, 一般用于进行数据比较或回滚
// 该方法一般在change事件中调用, change事件被触发后, _previousAttributes属性存放最新的数据
previous: function(attr) {
// 模型未执行过set(未触发过change事件)不会拥有属性_previousAttributes
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
// 在模型触发change事件中, 获取所有属性上一个状态的数据集合
// 该方法类似于previous()方法, 一般在change事件中调用, 用于数据比较或回滚
previousAttributes: function() {
// 将上一个状态的数据对象克隆为一个新对象并返回
return _.clone(this._previousAttributes);
},
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overridden,
// triggering a `"change"` event.
// 从服务器获取默认的模型数据, 获取数据后使用set方法将数据填充到模型, 因此如果获取到的数据与当前模型中的数据不一致, 将会触发change事件
fetch: function(options) {
// 确保options是一个新的对象, 随后将改变options中的属性
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
// 将model实例的指向赋值给一个变量,以期在子函数作用域中引用
var model = this;
// 可以在options中自定义success方法,在从服务器获取model数据成功后调用
var success = options.success;
// 成功回调函数,里面包裹用户自己定义的success函数
options.success = function(resp) {
// 通过parse方法将服务器返回的数据进行转换
// 通过set方法将转换后的数据填充到模型中, 因此可能会触发change事件(当数据发生变化时)
// 如果填充数据时验证失败, 则不会调用自定义success回调函数
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
// 触发model的sync事件(如果有添加监听的话)
model.trigger('sync', model, resp, options);
};
// 添加获取model数据失败或出错回调,并触发error事件(用户添加了监听的话)
wrapError(this, options);
// 调用model的sync并采取read的方式获取数据
return this.sync('read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
// 设置模型数据,并将数据同步到服务器。如果服务器返回了一个跟目前model不同的model数据,此时模型会重新将返回的model数据set给当前数据
save: function(key, val, options) {
var attrs, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments.
// 统一将参数处理为一个键值对和一个选项hash的格式
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
// If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
// 如果在options中设置了wait选项, 则被改变的数据将会被提前验证, 且服务器没有响应新数据(或响应失败)时, 本地数据会被还原为修改前的状态
// 如果没有设置wait选项, 如果数据验证失败则直接返回,否则无论服务器是否设置成功, 本地数据均会被修改为最新状态
if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
// 默认要做数据set验证
options = _.extend({validate: true}, options);
// Do not persist invalid models.
// 如果被设置的数据非法,则不进行同步操作
if (!this._validate(attrs, options)) return false;
// Set temporary attributes if `{wait: true}`.
// 如果设置了wait属性,则将attributes属性临时添加进attrs以便在后面扩展给model的attributes属性
if (attrs && options.wait) {
attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
// 如果服务端成功执行了保存,可以将客户端也更新为服务端的状态
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(resp) {
// Ensure attributes are restored during synchronous saves.
// 重置模型的attributes(被attrs扩展之前的状态)
model.attributes = attributes;
// 解析返回值(可自定义该parse方法)
var serverAttrs = model.parse(resp, options);
// 如果设置了外套参数,将从服务器返回的数据扩展给model。若有未设置成功的值则不执行自定义的成功回调
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
// 执行自定义的成功回调
if (success) success(model, resp, options);
// 触发sync事件
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
// 将模型中的数据保存到服务器
// 如果当前模型是一个新建的模型(没有id), 则使用create方法(新增), 若设置了patch方法则使用数据附加的方式更新,否则认为是update方法(修改)
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch') options.attrs = attrs;
xhr = this.sync(method, this, options);
// Restore attributes.
// 如果设置了options.wait, 则将数据还原为修改前的状态
// 此时保存的请求还没有得到响应, 因此如果响应失败, 模型中将保持修改前的状态, 如果服务器响应成功, 则会在success中设置模型中的数据为最新状态
if (attrs && options.wait) this.attributes = attributes;
return xhr;
},
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
// 删除模型, 模型将同时从所属的Collection集合中被删除
// 如果模型是在客户端新建的, 则直接从客户端删除
// 如果模型数据同时存在服务器, 则同时会删除服务器端的数据
destroy: function(options) {
// 保持配置数据是个全新的对象,这样就不会修改原有对象以免引发错误
options = options ? _.clone(options) : {};
var model = this;
// 缓存自定义成功回调函数
var success = options.success;
var destroy = function() {
// 触发destroy监听事件
model.trigger('destroy', model, model.collection, options);
};
// 添加成功回调
options.success = function(resp) {
// 如果设置了wait或者是新建的model,则直接触发destroy事件,如果模型存在于Collection集合中, 集合将监听destroy事件并在触发时从集合中移除该模型
// 删除模型时, 模型中的数据并没有被清空, 但模型已经从集合中移除, 因此当没有任何地方引用该模型时, 会被自动从内存中释放
// 建议在删除模型时, 将模型对象的引用变量设置为null
if (options.wait || model.isNew()) destroy();
// 执行自定义回调
if (success) success(model, resp, options);
// 如果model不是最新的,则触发sync事件
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
// 如果model是新的模型,则直接执行成功回调
if (this.isNew()) {
options.success();
return false;
}
wrapError(this, options);
// 从服务端执行删除操作
var xhr = this.sync('delete', this, options);
// 如果没有在options对象中配置wait项, 则会先删除本地数据, 再发送请求删除服务器数据
// 此时无论服务器删除是否成功, 本地模型数据已被删除
if (!options.wait) destroy();
return xhr;
},
// Default URL for the model's representation on the server -- if you're
// using Backbone's restful methods, override this to change the endpoint
// that will be called.
// 获取模型在服务器接口中对应的url, 在调用save, fetch, destroy等与服务器交互的方法时, 将使用该方法获取url
// 生成的url类似于"PATHINFO"模式, 服务器对模型的操作只有一个url, 对于修改和删除操作会在url后追加模型id便于标识
// 如果在模型中定义了urlRoot, 服务器接口应为[urlRoot/id]形式
// 如果模型所属的Collection集合定义了url方法或属性, 则使用集合中的url形式: [collection.url/id]
// 在访问服务器url时会在url后面追加上模型的id, 便于服务器标识一条记录, 因此模型中的id需要与服务器记录对应
// 如果无法获取模型或集合的url, 将调用urlError方法抛出一个异常
// 如果服务器接口并没有按照"PATHINFO"方式进行组织, 可以通过重载url方法实现与服务器的无缝交互
url: function() {
// 获取基本路径
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
// 如果当前模型是客户端新建的模型, 则不存在id属性, 服务器url直接使用base
if (this.isNew()) return base;
// 通过在基本路径末尾添加模型id的形式来做区别
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
},
// **parse** converts a response into the hash of attributes to be `set` on
// the model. The default implementation is just to pass the response along.
// parse方法用于解析从服务器获取的数据, 返回一个能够被set方法解析的模型数据
// 一般parse方法会根据服务器返回的数据进行重载, 以便构建与服务器的无缝连接
// 当服务器返回的数据结构与set方法所需的数据结构不一致(例如服务器返回XML格式数据时), 可使用parse方法进行转换
parse: function(resp, options) {
return resp;
},
// Create a new model with identical attributes to this one.
// 创建一个新的模型, 它具有和当前模型相同的数据
clone: function() {
return new this.constructor(this.attributes);
},
// A model is new if it has never been saved to the server, and lacks an id.
// 检查当前模型是否是客户端创建的新模型
// 检查方式是根据模型是否存在id标识, 客户端创建的新模型没有id标识
// 因此服务器响应的模型数据中必须包含id标识, 标识的属性名默认为"id", 也可以通过修改idAttribute属性自定义标识
isNew: function() {
return this.id == null;
},
// Check if the model is currently in a valid state.
// 验证当前模型中的数据是否能通过_validate方法验证
isValid: function(options) {
return this._validate({}, _.extend(options || {}, { validate: true }));
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
// 数据验证方法, 在调用set, save, add等数据更新方法时, 被自动执行
// 验证失败会触发模型对象的"error"事件, 如果在options中指定了error处理函数, 则只会执行options.error函数
// @param {Object} attrs 数据模型的attributes属性, 存储模型的对象化数据
// @param {Object} options 配置项
// @return {Boolean} 验证通过返回true, 不通过返回false
_validate: function(attrs, options) {
// 如果options没有设置validate或者没有给model设置validate函数,则默认返回true
if (!options.validate || !this.validate) return true;
// 将新传进来的参数对象扩展给attrs统一做验证
attrs = _.extend({}, this.attributes, attrs);
// 将验证结果传递给模型的validationError属性
var error = this.validationError = this.validate(attrs, options) || null;
// 验证通过(所以要注意自己的validate方法的返回值,成功的时候不作任何返回即可)
if (!error) return true;
// 如果对模型绑定了error事件监听, 则触发绑定事件
this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
return false;
}
});
// Underscore methods that we want to implement on the Model.
// 扩展underscore的方法给model
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
// Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) {
Model.prototype[method] = function() {
// 将参数对象转化为真正的数组
var args = slice.call(arguments);
// 将model实例的attributes加进数组,以便能被个方法调用
args.unshift(this.attributes);
// 执行underscore的同名方法并传进相应参数
return _[method].apply(_, args);
};
});
下一篇文章我们将对model的集合Collection进行详细的源码注释。