// Backbone.js 0.9.2 // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org (function() { // 创建一个全局对象, 在浏览器中表示为window对象, 在Node.js中表示global对象 var root = this; // 保存"Backbone"变量被覆盖之前的值 // 如果出现命名冲突或考虑到规范, 可通过Backbone.noConflict()方法恢复该变量被Backbone占用之前的值, 并返回Backbone对象以便重新命名 var previousBackbone = root.Backbone; // 将Array.prototype中的slice和splice方法缓存到局部变量以供调用 var slice = Array.prototype.slice; var splice = Array.prototype.splice; var Backbone; if( typeof exports !== 'undefined') { Backbone = exports; } else { Backbone = root.Backbone = {}; } // 定义Backbone版本 Backbone.VERSION = '0.9.2'; // 在服务器环境下自动导入Underscore, 在Backbone中部分方法依赖或继承自Underscore var _ = root._; if(!_ && ( typeof require !== 'undefined')) _ = require('underscore'); // 定义第三方库为统一的变量"$", 用于在视图(View), 事件处理和与服务器数据同步(sync)时调用库中的方法 // 支持的库包括jQuery, Zepto等, 它们语法相同, 但Zepto更适用移动开发, 它主要针对Webkit内核浏览器 // 也可以通过自定义一个与jQuery语法相似的自定义库, 供Backbone使用(有时我们可能需要一个比jQuery, Zepto更轻巧的自定义版本) // 这里定义的"$"是局部变量, 因此不会影响在Backbone框架之外第三方库的正常使用 var $ = root.jQuery || root.Zepto || root.ender; // 手动设置第三方库 // 如果在导入了Backbone之前并没有导入第三方库, 可以通过setDomLibrary方法设置"$"局部变量 // setDomLibrary方法也常用于在Backbone中动态导入自定义库 Backbone.setDomLibrary = function(lib) { $ = lib; }; // 放弃以"Backbone"命名框架, 并返回Backbone对象, 一般用于避免命名冲突或规范命名方式 // 例如: // var bk = Backbone.noConflict(); // 取消"Backbone"命名, 并将Backbone对象存放于bk变量中 // console.log(Backbone); // 该变量已经无法再访问Backbone对象, 而恢复为Backbone定义前的值 // var MyBackbone = bk; // 而bk存储了Backbone对象, 我们将它重命名为MyBackbone Backbone.noConflict = function() { root.Backbone = previousBackbone; return this; }; // 对于不支持REST方式的浏览器, 可以设置Backbone.emulateHTTP = true // 与服务器请求将以POST方式发送, 并在数据中加入_method参数标识操作名称, 同时也将发送X-HTTP-Method-Override头信息 Backbone.emulateHTTP = false; // 对于不支持application/json编码的浏览器, 可以设置Backbone.emulateJSON = true; // 将请求类型设置为application/x-www-form-urlencoded, 并将数据放置在model参数中实现兼容 Backbone.emulateJSON = false; // Backbone.Events 自定义事件相关 // ----------------- // eventSplitter指定处理多个事件时, 事件名称的解析规则 var eventSplitter = /\s+/; // 自定义事件管理器 // 通过在对象中绑定Events相关方法, 允许向对象添加, 删除和触发自定义事件 var Events = Backbone.Events = { // 将自定义事件(events)和回调函数(callback)绑定到当前对象 // 回调函数中的上下文对象为指定的context, 如果没有设置context则上下文对象默认为当前绑定事件的对象 // 该方法类似与DOM Level2中的addEventListener方法 // events允许指定多个事件名称, 通过空白字符进行分隔(如空格, 制表符等) // 当事件名称为"all"时, 在调用trigger方法触发任何事件时, 均会调用"all"事件中绑定的所有回调函数 on : function(events, callback, context) { // 定义一些函数中使用到的局部变量 var calls, event, node, tail, list; // 必须设置callback回调函数 if(!callback) return this; // 通过eventSplitter对事件名称进行解析, 使用split将多个事件名拆分为一个数组 // 一般使用空白字符指定多个事件名称 events = events.split(eventSplitter); // calls记录了当前对象中已绑定的事件与回调函数列表 calls = this._callbacks || (this._callbacks = {}); // 循环事件名列表, 从头至尾依次将事件名存放至event变量 while( event = events.shift()) { // 获取已经绑定event事件的回调函数 // list存储单个事件名中绑定的callback回调函数列表 // 函数列表并没有通过数组方式存储, 而是通过多个对象的next属性进行依次关联 /** 数据格式如: * { * tail: {Object}, * next: { * callback: {Function}, * context: {Object}, * next: { * callback: {Function}, * context: {Object}, * next: {Object} * } * } * } */ // 列表每一层next对象存储了一次回调事件相关信息(函数体, 上下文和下一次回调事件) // 事件列表最顶层存储了一个tail对象, 它存储了最后一次绑定回调事件的标识(与最后一次回调事件的next指向同一个对象) // 通过tail标识, 可以在遍历回调列表时得知已经到达最后一个回调函数 list = calls[event]; // node变量用于记录本次回调函数的相关信息 // tail只存储最后一次绑定回调函数的标识 // 因此如果之前已经绑定过回调函数, 则将之前的tail指定给node作为一个对象使用, 然后创建一个新的对象标识给tail // 这里之所以要将本次回调事件添加到上一次回调的tail对象, 是为了让回调函数列表的对象层次关系按照绑定顺序排列(最新绑定的事件将被放到最底层) node = list ? list.tail : {}; node.next = tail = {}; // 记录本次回调的函数体及上下文信息 node.context = context; node.callback = callback; // 重新组装当前事件的回调列表, 列表中已经加入了本次回调事件 calls[event] = { tail : tail, next : list ? list.next : node }; } // 返回当前对象, 方便进行方法链调用 return this; }, // 移除对象中已绑定的事件或回调函数, 可以通过events, callback和context对需要删除的事件或回调函数进行过滤 // - 如果context为空, 则移除所有的callback指定的函数 // - 如果callback为空, 则移除事件中所有的回调函数 // - 如果events为空, 但指定了callback或context, 则移除callback或context指定的回调函数(不区分事件名称) // - 如果没有传递任何参数, 则移除对象中绑定的所有事件和回调函数 off : function(events, callback, context) { var event, calls, node, tail, cb, ctx; // No events, or removing *all* events. // 当前对象没有绑定任何事件 if(!( calls = this._callbacks)) return; // 如果没有指定任何参数, 则移除所有事件和回调函数(删除_callbacks属性) if(!(events || callback || context)) { delete this._callbacks; return this; } // 解析需要移除的事件列表 // - 如果指定了events, 则按照eventSplitter对事件名进行解析 // - 如果没有指定events, 则解析已绑定所有事件的名称列表 events = events ? events.split(eventSplitter) : _.keys(calls); // 循环事件名列表 while( event = events.shift()) { // 将当前事件对象从列表中移除, 并缓存到node变量中 node = calls[event]; delete calls[event]; // 如果不存在当前事件对象(或没有指定移除过滤条件, 则认为将移除当前事件及所有回调函数), 则终止此次操作(事件对象在上一步已经移除) if(!node || !(callback || context)) continue; // Create a new list, omitting the indicated callbacks. // 根据回调函数或上下文过滤条件, 组装一个新的事件对象并重新绑定 tail = node.tail; // 遍历事件中的所有回调对象 while(( node = node.next) !== tail) { cb = node.callback; ctx = node.context; // 根据参数中的回调函数和上下文, 对回调函数进行过滤, 将不符合过滤条件的回调函数重新绑定到事件中(因为事件中的所有回调函数在上面已经被移除) if((callback && cb !== callback) || (context && ctx !== context)) { this.on(event, cb, ctx); } } } return this; }, // 触发已经定义的一个或多个事件, 依次执行绑定的回调函数列表 trigger : function(events) { var event, node, calls, tail, args, all, rest; // 当前对象没有绑定任何事件 if(!( calls = this._callbacks)) return this; // 获取回调函数列表中绑定的"all"事件列表 all = calls.all; // 将需要触发的事件名称, 按照eventSplitter规则解析为一个数组 events = events.split(eventSplitter); // 将trigger从第2个之后的参数, 记录到rest变量, 将依次传递给回调函数 rest = slice.call(arguments, 1); // 循环需要触发的事件列表 while( event = events.shift()) { // 此处的node变量记录了当前事件的所有回调函数列表 if( node = calls[event]) { // tail变量记录最后一次绑定事件的对象标识 tail = node.tail; // node变量的值, 按照事件的绑定顺序, 被依次赋值为绑定的单个回调事件对象 // 最后一次绑定的事件next属性, 与tail引用同一个对象, 以此作为是否到达列表末尾的判断依据 while(( node = node.next) !== tail) { // 执行所有绑定的事件, 并将调用trigger时的参数传递给回调函数 node.callback.apply(node.context || this, rest); } } // 变量all记录了绑定时的"all"事件, 即在调用任何事件时, "all"事件中的回调函数均会被执行 // - "all"事件中的回调函数无论绑定顺序如何, 都会在当前事件的回调函数列表全部执行完毕后再依次执行 // - "all"事件应该在触发普通事件时被自动调用, 如果强制触发"all"事件, 事件中的回调函数将被执行两次 if( node = all) { tail = node.tail; // 与调用普通事件的回调函数不同之处在于, all事件会将当前调用的事件名作为第一个参数传递给回调函数 args = [event].concat(rest); // 遍历并执行"all"事件中的回调函数列表 while(( node = node.next) !== tail) { node.callback.apply(node.context || this, args); } } } return this; } }; // 绑定事件与释放事件的别名, 也为了同时兼容Backbone以前的版本 Events.bind = Events.on; Events.unbind = Events.off; // Backbone.Model 数据对象模型 // -------------- // Model是Backbone中所有数据对象模型的基类, 用于创建一个数据模型 // @param {Object} attributes 指定创建模型时的初始化数据 // @param {Object} options /** * @format options * { * parse: {Boolean}, * collection: {Collection} * } */ var Model = Backbone.Model = function(attributes, options) { // defaults变量用于存储模型的默认数据 var defaults; // 如果没有指定attributes参数, 则设置attributes为空对象 attributes || ( attributes = {}); // 设置attributes默认数据的解析方法, 例如默认数据是从服务器获取(或原始数据是XML格式), 为了兼容set方法所需的数据格式, 可使用parse方法进行解析 if(options && options.parse) attributes = this.parse(attributes); if( defaults = getValue(this, 'defaults')) { // 如果Model在定义时设置了defaults默认数据, 则初始化数据使用defaults与attributes参数合并后的数据(attributes中的数据会覆盖defaults中的同名数据) attributes = _.extend({}, defaults, attributes); } // 显式指定模型所属的Collection对象(在调用Collection的add, push等将模型添加到集合中的方法时, 会自动设置模型所属的Collection对象) if(options && options.collection) this.collection = options.collection; // attributes属性存储了当前模型的JSON对象化数据, 创建模型时默认为空 this.attributes = {}; // 定义_escapedAttributes缓存对象, 它将缓存通过escape方法处理过的数据 this._escapedAttributes = {}; // 为每一个模型配置一个唯一标识 this.cid = _.uniqueId('c'); // 定义一系列用于记录数据状态的对象, 具体含义请参考对象定义时的注释 this.changed = {}; this._silent = {}; this._pending = {}; // 创建实例时设置初始化数据, 首次设置使用silent参数, 不会触发change事件 this.set(attributes, { silent : true }); // 上面已经设置了初始化数据, changed, _silent, _pending对象的状态可能已经发生变化, 这里重新进行初始化 this.changed = {}; this._silent = {}; this._pending = {}; // _previousAttributes变量存储模型数据的一个副本 // 用于在change事件中获取模型数据被改变之前的状态, 可通过previous或previousAttributes方法获取上一个状态的数据 this._previousAttributes = _.clone(this.attributes); // 调用initialize初始化方法 this.initialize.apply(this, arguments); }; // 使用extend方法为Model原型定义一系列属性和方法 _.extend(Model.prototype, Events, { // changed属性记录了每次调用set方法时, 被改变数据的key集合 changed : null, // // 当指定silent属性时, 不会触发change事件, 被改变的数据会记录下来, 直到下一次触发change事件 // _silent属性用来记录使用silent时的被改变的数据 _silent : null, _pending : null, // 每个模型的唯一标识属性(默认为"id", 通过修改idAttribute可自定义id属性名) // 如果在设置数据时包含了id属性, 则id将会覆盖模型的id // id用于在Collection集合中查找和标识模型, 与后台接口通信时也会以id作为一条记录的标识 idAttribute : 'id', // 模型初始化方法, 在模型被构造结束后自动调用 initialize : function() { }, // 返回当前模型中数据的一个副本(JSON对象格式) toJSON : function(options) { return _.clone(this.attributes); }, // 根据attr属性名, 获取模型中的数据值 get : function(attr) { return this.attributes[attr]; }, // 根据attr属性名, 获取模型中的数据值, 数据值包含的HTML特殊字符将被转换为HTML实体, 包含 & " ' \ // 通过 _.escape方法实现 escape : function(attr) { var html; // 从_escapedAttributes缓存对象中查找数据, 如果数据已经被缓存则直接返回 if( html = this._escapedAttributes[attr]) return html; // _escapedAttributes缓存对象中没有找到数据 // 则先从模型中获取数据 var val = this.get(attr); // 将数据中的HTML使用 _.escape方法转换为实体, 并缓存到_escapedAttributes对象, 便于下次直接获取 return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); }, // 检查模型中是否存在某个属性, 当该属性的值被转换为Boolean类型后值为false, 则认为不存在 // 如果值为false, null, undefined, 0, NaN, 或空字符串时, 均会被转换为false has : function(attr) { return this.get(attr) != null; }, // 设置模型中的数据, 如果key值不存在, 则作为新的属性添加到模型, 如果key值已经存在, 则修改为新的值 set : function(key, value, options) { // attrs变量中记录需要设置的数据对象 var attrs, attr, val; // 参数形式允许key-value对象形式, 或通过key, value两个参数进行单独设置 // 如果key是一个对象, 则认定为使用对象形式设置, 第二个参数将被视为options参数 if(_.isObject(key) || key == null) { attrs = key; options = value; } else { // 通过key, value两个参数单独设置, 将数据放到attrs对象中方便统一处理 attrs = {}; attrs[key] = value; } // options配置项必须是一个对象, 如果没有设置options则默认值为一个空对象 options || ( options = {}); // 没有设置参数时不执行任何动作 if(!attrs) return this; // 如果被设置的数据对象属于Model类的一个实例, 则将Model对象的attributes数据对象赋给attrs // 一般在复制一个Model对象的数据到另一个Model对象时, 会执行该动作 if( attrs instanceof Model) attrs = attrs.attributes; // 如果options配置对象中设置了unset属性, 则将attrs数据对象中的所有属性重置为undefined // 一般在复制一个Model对象的数据到另一个Model对象时, 但仅仅需要复制Model中的数据而不需要复制值时执行该操作 if(options.unset) for(attr in attrs) attrs[attr] = void 0; // 对当前数据进行验证, 如果验证未通过则停止执行 if(!this._validate(attrs, options)) return false; // 如果设置的id属性名被包含在数据集合中, 则将id覆盖到模型的id属性 // 这是为了确保在自定义id属性名后, 访问模型的id属性时, 也能正确访问到id if(this.idAttribute in attrs) this.id = attrs[this.idAttribute]; var changes = options.changes = {}; // now记录当前模型中的数据对象 var now = this.attributes; // escaped记录当前模型中通过escape缓存过的数据 var escaped = this._escapedAttributes; // prev记录模型中数据被改变之前的值 var prev = this._previousAttributes || {}; // 遍历需要设置的数据对象 for(attr in attrs) { // attr存储当前属性名称, val存储当前属性的值 val = attrs[attr]; // 如果当前数据在模型中不存在, 或已经发生变化, 或在options中指定了unset属性删除, 则删除该数据被换存在_escapedAttributes中的数据 if(!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { // 仅删除通过escape缓存过的数据, 这是为了保证缓存中的数据与模型中的真实数据保持同步 delete escaped[attr]; // 如果指定了silent属性, 则此次set方法调用不会触发change事件, 因此将被改变的数据记录到_silent属性中, 便于下一次触发change事件时, 通知事件监听函数此数据已经改变 // 如果没有指定silent属性, 则直接设置changes属性中当前数据为已改变状态 (options.silent ? this._silent : changes)[attr] = true; } // 如果在options中设置了unset, 则从模型中删除该数据(包括key) // 如果没有指定unset属性, 则认为将新增或修改数据, 向模型的数据对象中加入新的数据 options.unset ? delete now[attr] : now[attr] = val; // 如果模型中的数据与新的数据不一致, 则表示该数据已发生变化 if(!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { // 在changed属性中记录当前属性已经发生变化的状态 this.changed[attr] = val; if(!options.silent) this._pending[attr] = true; } else { // 如果数据没有发生变化, 则从changed属性中移除已变化状态 delete this.changed[attr]; delete this._pending[attr]; } } // 调用change方法, 将触发change事件绑定的函数 if(!options.silent) this.change(options); return this; }, // 从当前模型中删除指定的数据(属性也将被同时删除) unset : function(attr, options) { (options || ( options = {})).unset = true; // 通过options.unset配置项告知set方法进行删除操作 return this.set(attr, null, options); }, // 清除当前模型中的所有数据和属性 clear : function(options) { (options || ( options = {})).unset = true; // 克隆一个当前模型的属性副本, 并通过options.unset配置项告知set方法执行删除操作 return this.set(_.clone(this.attributes), options); }, // 从服务器获取默认的模型数据, 获取数据后使用set方法将数据填充到模型, 因此如果获取到的数据与当前模型中的数据不一致, 将会触发change事件 fetch : function(options) { // 确保options是一个新的对象, 随后将改变options中的属性 options = options ? _.clone(options) : {}; var model = this; // 在options中可以指定获取数据成功后的自定义回调函数 var success = options.success; // 当获取数据成功后填充数据并调用自定义成功回调函数 options.success = function(resp, status, xhr) { // 通过parse方法将服务器返回的数据进行转换 // 通过set方法将转换后的数据填充到模型中, 因此可能会触发change事件(当数据发生变化时) // 如果填充数据时验证失败, 则不会调用自定义success回调函数 if(!model.set(model.parse(resp, xhr), options)) return false; // 调用自定义的success回调函数 if(success) success(model, resp); }; // 请求发生错误时通过wrapError处理error事件 options.error = Backbone.wrapError(options.error, model, options); // 调用sync方法从服务器获取数据 return (this.sync || Backbone.sync).call(this, 'read', this, options); }, // 保存模型中的数据到服务器 save : function(key, value, options) { // attrs存储需要保存到服务器的数据对象 var attrs, current; // 支持设置单个属性的方式 key: value // 支持对象形式的批量设置方式 {key: value} if(_.isObject(key) || key == null) { // 如果key是一个对象, 则认为是通过对象方式设置 // 此时第二个参数被认为是options attrs = key; options = value; } else { // 如果是通过key: value形式设置单个属性, 则直接设置attrs attrs = {}; attrs[key] = value; } // 配置对象必须是一个新的对象 options = options ? _.clone(options) : {}; // 如果在options中设置了wait选项, 则被改变的数据将会被提前验证, 且服务器没有响应新数据(或响应失败)时, 本地数据会被还原为修改前的状态 // 如果没有设置wait选项, 则无论服务器是否设置成功, 本地数据均会被修改为最新状态 if(options.wait) { // 对需要保存的数据提前进行验证 if(!this._validate(attrs, options)) return false; // 记录当前模型中的数据, 用于在将数据发送到服务器后, 将数据进行还原 // 如果服务器响应失败或没有返回数据, 则可以保持修改前的状态 current = _.clone(this.attributes); } // silentOptions在options对象中加入了silent(不对数据进行验证) // 当使用wait参数时使用silentOptions配置项, 因为在上面已经对数据进行过验证 // 如果没有设置wait参数, 则仍然使用原始的options配置项 var silentOptions = _.extend({}, options, { silent : true }); // 将修改过最新的数据保存到模型中, 便于在sync方法中获取模型数据保存到服务器 if(attrs && !this.set(attrs, options.wait ? silentOptions : options)) { return false; } var model = this; // 在options中可以指定保存数据成功后的自定义回调函数 var success = options.success; // 服务器响应成功后执行success options.success = function(resp, status, xhr) { // 获取服务器响应最新状态的数据 var serverAttrs = model.parse(resp, xhr); // 如果使用了wait参数, 则优先将修改后的数据状态直接设置到模型 if(options.wait) { delete options.wait; serverAttrs = _.extend(attrs || {}, serverAttrs); } // 将最新的数据状态设置到模型中 // 如果调用set方法时验证失败, 则不会调用自定义的success回调函数 if(!model.set(serverAttrs, options)) return false; if(success) { // 调用响应成功后自定义的success回调函数 success(model, resp); } else { // 如果没有指定自定义回调, 则默认触发sync事件 model.trigger('sync', model, resp, options); } }; // 请求发生错误时通过wrapError处理error事件 options.error = Backbone.wrapError(options.error, model, options); // 将模型中的数据保存到服务器 // 如果当前模型是一个新建的模型(没有id), 则使用create方法(新增), 否则认为是update方法(修改) var method = this.isNew() ? 'create' : 'update'; var xhr = (this.sync || Backbone.sync).call(this, method, this, options); // 如果设置了options.wait, 则将数据还原为修改前的状态 // 此时保存的请求还没有得到响应, 因此如果响应失败, 模型中将保持修改前的状态, 如果服务器响应成功, 则会在success中设置模型中的数据为最新状态 if(options.wait) this.set(current, silentOptions); return xhr; }, // 删除模型, 模型将同时从所属的Collection集合中被删除 // 如果模型是在客户端新建的, 则直接从客户端删除 // 如果模型数据同时存在服务器, 则同时会删除服务器端的数据 destroy : function(options) { // 配置项必须是一个新的对象 options = options ? _.clone(options) : {}; var model = this; // 在options中可以指定删除数据成功后的自定义回调函数 var success = options.success; // 删除数据成功调用, 触发destroy事件, 如果模型存在于Collection集合中, 集合将监听destroy事件并在触发时从集合中移除该模型 // 删除模型时, 模型中的数据并没有被清空, 但模型已经从集合中移除, 因此当没有任何地方引用该模型时, 会被自动从内存中释放 // 建议在删除模型时, 将模型对象的引用变量设置为null var triggerDestroy = function() { model.trigger('destroy', model, model.collection, options); }; // 如果该模型是一个客户端新建的模型, 则直接调用triggerDestroy从集合中将模型移除 if(this.isNew()) { triggerDestroy(); return false; } // 当从服务器删除数据成功时 options.success = function(resp) { // 如果在options对象中配置wait项, 则表示本地内存中的模型数据, 会在服务器数据被删除成功后再删除 // 如果服务器响应失败, 则本地数据不会被删除 if(options.wait) triggerDestroy(); if(success) { // 调用自定义的成功回调函数 success(model, resp); } else { // 如果没有自定义回调, 则默认触发sync事件 model.trigger('sync', model, resp, options); } }; // 请求发生错误时通过wrapError处理error事件 options.error = Backbone.wrapError(options.error, model, options); // 通过sync方法发送删除数据的请求 var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); // 如果没有在options对象中配置wait项, 则会先删除本地数据, 再发送请求删除服务器数据 // 此时无论服务器删除是否成功, 本地模型数据已被删除 if(!options.wait) triggerDestroy(); return xhr; }, // 获取模型在服务器接口中对应的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() { // 定义服务器对应的url路径 var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError(); // 如果当前模型是客户端新建的模型, 则不存在id属性, 服务器url直接使用base if(this.isNew()) return base; // 如果当前模型具有id属性, 可能是调用了save或destroy方法, 将在base后面追加模型的id // 下面将判断base最后一个字符是否是"/", 生成的url格式为[base/id] return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); }, // parse方法用于解析从服务器获取的数据, 返回一个能够被set方法解析的模型数据 // 一般parse方法会根据服务器返回的数据进行重载, 以便构建与服务器的无缝连接 // 当服务器返回的数据结构与set方法所需的数据结构不一致(例如服务器返回XML格式数据时), 可使用parse方法进行转换 parse : function(resp, xhr) { return resp; }, // 创建一个新的模型, 它具有和当前模型相同的数据 clone : function() { return new this.constructor(this.attributes); }, // 检查当前模型是否是客户端创建的新模型 // 检查方式是根据模型是否存在id标识, 客户端创建的新模型没有id标识 // 因此服务器响应的模型数据中必须包含id标识, 标识的属性名默认为"id", 也可以通过修改idAttribute属性自定义标识 isNew : function() { return this.id == null; }, // 数据被更新时触发change事件绑定的函数 // 当set方法被调用, 会自动调用change方法, 如果在set方法被调用时指定了silent配置, 则需要手动调用change方法 change : function(options) { // options必须是一个对象 options || ( options = {}); // this._changing相关的逻辑有些问题 // this._changing在方法最后被设置为false, 因此方法上面changing变量的值始终为false(第一次为undefined) // 作者的初衷应该是想用该变量标示change方法是否执行完毕, 对于浏览器端单线程的脚本来说没有意义, 因为该方法被执行时会阻塞其它脚本 // changing获取上一次执行的状态, 如果上一次脚本没有执行完毕, 则值为true var changing = this._changing; // 开始执行标识, 执行过程中值始终为true, 执行完毕后this._changing被修改为false this._changing = true; // 将非本次改变的数据状态添加到_pending对象中 for(var attr in this._silent) this._pending[attr] = true; // changes对象包含了当前数据上一次执行change事件至今, 已被改变的所有数据 // 如果之前使用silent未触发change事件, 则本次会被放到changes对象中 var changes = _.extend({}, options.changes, this._silent); // 重置_silent对象 this._silent = {}; // 遍历changes对象, 分别针对每一个属性触发单独的change事件 for(var attr in changes) { // 将Model对象, 属性值, 配置项作为参数以此传递给事件的监听函数 this.trigger('change:' + attr, this, this.get(attr), options); } // 如果方法处于执行中, 则停止执行 if(changing) return this; // 触发change事件, 任意数据被改变后, 都会依次触发"change:属性"事件和"change"事件 while(!_.isEmpty(this._pending)) { this._pending = {}; // 触发change事件, 并将Model实例和配置项作为参数传递给监听函数 this.trigger('change', this, options); // 遍历changed对象中的数据, 并依次将已改变数据的状态从changed中移除 // 在此之后如果调用hasChanged检查数据状态, 将得到false(未改变) for(var attr in this.changed) { if(this._pending[attr] || this._silent[attr]) continue; // 移除changed中数据的状态 delete this.changed[attr]; } // change事件执行完毕, _previousAttributes属性将记录当前模型最新的数据副本 // 因此如果需要获取数据的上一个状态, 一般只通过在触发的change事件中通过previous或previousAttributes方法获取 this._previousAttributes = _.clone(this.attributes); } // 执行完毕标识 this._changing = false; return this; }, // 检查某个数据是否在上一次执行change事件后被改变过 /** * 一般在change事件中配合previous或previousAttributes方法使用, 如: * if(model.hasChanged('attr')) { * var attrPrev = model.previous('attr'); * } */ hasChanged : function(attr) { if(!arguments.length) return !_.isEmpty(this.changed); return _.has(this.changed, attr); }, // 获取当前模型中的数据与上一次数据中已经发生变化的数据集合 // (一般在使用silent属性时没有调用change方法, 因此数据会被临时抱存在changed属性中, 上一次的数据可通过previousAttributes方法获取) // 如果传递了diff集合, 将使用上一次模型数据与diff集合中的数据进行比较, 返回不一致的数据集合 // 如果比较结果中没有差异, 则返回false changedAttributes : function(diff) { // 如果没有指定diff, 将返回当前模型较上一次状态已改变的数据集合, 这些数据已经被存在changed属性中, 因此返回changed集合的一个副本 if(!diff) return this.hasChanged() ? _.clone(this.changed) : false; // 指定了需要进行比较的diff集合, 将返回上一次的数据与diff集合的比较结果 // old变量存储了上一个状态的模型数据 var val, changed = false, old = this._previousAttributes; // 遍历diff集合, 并将每一项与上一个状态的集合进行比较 for(var attr in diff) { // 将比较结果不一致的数据临时存储到changed变量 if(_.isEqual(old[attr], ( val = diff[attr]))) continue; (changed || (changed = {}))[attr] = val; } // 返回比较结果 return changed; }, // 在模型触发的change事件中, 获取某个属性被改变前上一个状态的数据, 一般用于进行数据比较或回滚 // 该方法一般在change事件中调用, change事件被触发后, _previousAttributes属性存放最新的数据 previous : function(attr) { // attr指定需要获取上一个状态的属性名称 if(!arguments.length || !this._previousAttributes) return null; return this._previousAttributes[attr]; }, // 在模型触发change事件中, 获取所有属性上一个状态的数据集合 // 该方法类似于previous()方法, 一般在change事件中调用, 用于数据比较或回滚 previousAttributes : function() { // 将上一个状态的数据对象克隆为一个新对象并返回 return _.clone(this._previousAttributes); }, // Check if the model is currently in a valid state. It's only possible to // get into an *invalid* state if you're using silent changes. // 验证当前模型中的数据是否能通过validate方法验证, 调用前请确保定义了validate方法 isValid : function() { return !this.validate(this.attributes); }, // 数据验证方法, 在调用set, save, add等数据更新方法时, 被自动执行 // 验证失败会触发模型对象的"error"事件, 如果在options中指定了error处理函数, 则只会执行options.error函数 // @param {Object} attrs 数据模型的attributes属性, 存储模型的对象化数据 // @param {Object} options 配置项 // @return {Boolean} 验证通过返回true, 不通过返回false _validate : function(attrs, options) { // 如果在调用set, save, add等数据更新方法时设置了options.silent属性, 则忽略验证 // 如果Model中没有添加validate方法, 则忽略验证 if(options.silent || !this.validate) return true; // 获取对象中所有的属性值, 并放入validate方法中进行验证 // validate方法包含2个参数, 分别为模型中的数据集合与配置对象, 如果验证通过则不返回任何数据(默认为undefined), 验证失败则返回带有错误信息数据 attrs = _.extend({}, this.attributes, attrs); var error = this.validate(attrs, options); // 验证通过 if(!error) return true; // 验证未通过 // 如果配置对象中设置了error错误处理方法, 则调用该方法并将错误数据和配置对象传递给该方法 if(options && options.error) { options.error(this, error, options); } else { // 如果对模型绑定了error事件监听, 则触发绑定事件 this.trigger('error', this, error, options); } // 返回验证未通过标识 return false; } }); // Backbone.Collection 数据模型集合相关 // ------------------- // Collection集合存储一系列相同类的数据模型, 并提供相关方法对模型进行操作 var Collection = Backbone.Collection = function(models, options) { // 配置对象 options || ( options = {}); // 在配置参数中设置集合的模型类 if(options.model) this.model = options.model; // 如果设置了comparator属性, 则集合中的数据将按照comparator方法中的排序算法进行排序(在add方法中会自动调用) if(options.comparator) this.comparator = options.comparator; // 实例化时重置集合的内部状态(第一次调用时可理解为定义状态) this._reset(); // 调用自定义初始化方法, 如果需要一般会重载initialize方法 this.initialize.apply(this, arguments); // 如果指定了models数据, 则调用reset方法将数据添加到集合中 // 首次调用时设置了silent参数, 因此不会触发"reset"事件 if(models) this.reset(models, { silent : true, parse : options.parse }); }; // 通过extend方法定义集合类原型方法 _.extend(Collection.prototype, Events, { // 定义集合的模型类, 模型类必须是一个Backbone.Model的子类 // 在使用集合相关方法(如add, create等)时, 允许传入数据对象, 集合方法会根据定义的模型类自动创建对应的实例 // 集合中存储的数据模型应该都是同一个模型类的实例 model : Model, // 初始化方法, 该方法在集合实例被创建后自动调用 // 一般会在定义集合类时重载该方法 initialize : function() { }, // 返回一个数组, 包含了集合中每个模型的数据对象 toJSON : function(options) { // 通过Undersocre的map方法将集合中每一个模型的toJSON结果组成一个数组, 并返回 return this.map(function(model) { // 依次调用每个模型对象的toJSON方法, 该方法默认将返回模型的数据对象(复制的副本) // 如果需要返回字符串等其它形式, 可以重载toJSON方法 return model.toJSON(options); }); }, // 向集合中添加一个或多个模型对象 // 默认会触发"add"事件, 如果在options中设置了silent属性, 可以关闭此次事件触发 // 传入的models可以是一个或一系列的模型对象(Model类的实例), 如果在集合中设置了model属性, 则允许直接传入数据对象(如 {name: 'test'}), 将自动将数据对象实例化为model指向的模型对象 add : function(models, options) { // 局部变量定义 var i, index, length, model, cid, id, cids = {}, ids = {}, dups = []; options || ( options = {}); // models必须是一个数组, 如果只传入了一个模型, 则将其转换为数组 models = _.isArray(models) ? models.slice() : [models]; // 遍历需要添加的模型列表, 遍历过程中, 将执行以下操作: // - 将数据对象转化模型对象 // - 建立模型与集合之间的引用 // - 记录无效和重复的模型, 并在后面进行过滤 for( i = 0, length = models.length; i