//Backbone.js0.9.2
//(c)2010-2012JeremyAshkenas,DocumentCloudInc.
//BackbonemaybefreelydistributedundertheMITlicense.
//Foralldetailsanddocumentation:
//http://backbonejs.org
(function(){
//创建一个全局对象,在浏览器中表示为window对象,在Node.js中表示global对象
varroot=this;
//保存"Backbone"变量被覆盖之前的值
//如果出现命名冲突或考虑到规范,可通过Backbone.noConflict()方法恢复该变量被Backbone占用之前的值,并返回Backbone对象以便重新命名
varpreviousBackbone=root.Backbone;
//将Array.prototype中的slice和splice方法缓存到局部变量以供调用
varslice=Array.prototype.slice;
varsplice=Array.prototype.splice;
varBackbone;
if(typeofexports!=='undefined'){
Backbone=exports;
}else{
Backbone=root.Backbone={};
}
//定义Backbone版本
Backbone.VERSION='0.9.2';
//在服务器环境下自动导入Underscore,在Backbone中部分方法依赖或继承自Underscore
var_=root._;
if(!_&&(typeofrequire!=='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对象,一般用于避免命名冲突或规范命名方式
//例如:
//varbk=Backbone.noConflict();//取消"Backbone"命名,并将Backbone对象存放于bk变量中
//console.log(Backbone);//该变量已经无法再访问Backbone对象,而恢复为Backbone定义前的值
//varMyBackbone=bk;//而bk存储了Backbone对象,我们将它重命名为MyBackbone
Backbone.noConflict=function(){
root.Backbone=previousBackbone;
returnthis;
};
//对于不支持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指定处理多个事件时,事件名称的解析规则
vareventSplitter=/\s+/;
//自定义事件管理器
//通过在对象中绑定Events相关方法,允许向对象添加,删除和触发自定义事件
varEvents=Backbone.Events={
//将自定义事件(events)和回调函数(callback)绑定到当前对象
//回调函数中的上下文对象为指定的context,如果没有设置context则上下文对象默认为当前绑定事件的对象
//该方法类似与DOMLevel2中的addEventListener方法
//events允许指定多个事件名称,通过空白字符进行分隔(如空格,制表符等)
//当事件名称为"all"时,在调用trigger方法触发任何事件时,均会调用"all"事件中绑定的所有回调函数
on:function(events,callback,context){
//定义一些函数中使用到的局部变量
varcalls,event,node,tail,list;
//必须设置callback回调函数
if(!callback)
returnthis;
//通过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
};
}
//返回当前对象,方便进行方法链调用
returnthis;
},
//移除对象中已绑定的事件或回调函数,可以通过events,callback和context对需要删除的事件或回调函数进行过滤
//-如果context为空,则移除所有的callback指定的函数
//-如果callback为空,则移除事件中所有的回调函数
//-如果events为空,但指定了callback或context,则移除callback或context指定的回调函数(不区分事件名称)
//-如果没有传递任何参数,则移除对象中绑定的所有事件和回调函数
off:function(events,callback,context){
varevent,calls,node,tail,cb,ctx;
//Noevents,orremoving*all*events.
//当前对象没有绑定任何事件
if(!(calls=this._callbacks))
return;
//如果没有指定任何参数,则移除所有事件和回调函数(删除_callbacks属性)
if(!(events||callback||context)){
deletethis._callbacks;
returnthis;
}
//解析需要移除的事件列表
//-如果指定了events,则按照eventSplitter对事件名进行解析
//-如果没有指定events,则解析已绑定所有事件的名称列表
events=events?events.split(eventSplitter):_.keys(calls);
//循环事件名列表
while(event=events.shift()){
//将当前事件对象从列表中移除,并缓存到node变量中
node=calls[event];
deletecalls[event];
//如果不存在当前事件对象(或没有指定移除过滤条件,则认为将移除当前事件及所有回调函数),则终止此次操作(事件对象在上一步已经移除)
if(!node||!(callback||context))
continue;
//Createanewlist,omittingtheindicatedcallbacks.
//根据回调函数或上下文过滤条件,组装一个新的事件对象并重新绑定
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);
}
}
}
returnthis;
},
//触发已经定义的一个或多个事件,依次执行绑定的回调函数列表
trigger:function(events){
varevent,node,calls,tail,args,all,rest;
//当前对象没有绑定任何事件
if(!(calls=this._callbacks))
returnthis;
//获取回调函数列表中绑定的"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);
}
}
}
returnthis;
}
};
//绑定事件与释放事件的别名,也为了同时兼容Backbone以前的版本
Events.bind=Events.on;
Events.unbind=Events.off;
//Backbone.Model数据对象模型
//--------------
//Model是Backbone中所有数据对象模型的基类,用于创建一个数据模型
//@param{Object}attributes指定创建模型时的初始化数据
//@param{Object}options
/**
*@formatoptions
*{
*parse:{Boolean},
*collection:{Collection}
*}
*/
varModel=Backbone.Model=function(attributes,options){
//defaults变量用于存储模型的默认数据
vardefaults;
//如果没有指定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){
returnthis.attributes[attr];
},
//根据attr属性名,获取模型中的数据值,数据值包含的HTML特殊字符将被转换为HTML实体,包含&<>"'\
//通过_.escape方法实现
escape:function(attr){
varhtml;
//从_escapedAttributes缓存对象中查找数据,如果数据已经被缓存则直接返回
if(html=this._escapedAttributes[attr])
returnhtml;
//_escapedAttributes缓存对象中没有找到数据
//则先从模型中获取数据
varval=this.get(attr);
//将数据中的HTML使用_.escape方法转换为实体,并缓存到_escapedAttributes对象,便于下次直接获取
returnthis._escapedAttributes[attr]=_.escape(val==null?'':''+val);
},
//检查模型中是否存在某个属性,当该属性的值被转换为Boolean类型后值为false,则认为不存在
//如果值为false,null,undefined,0,NaN,或空字符串时,均会被转换为false
has:function(attr){
returnthis.get(attr)!=null;
},
//设置模型中的数据,如果key值不存在,则作为新的属性添加到模型,如果key值已经存在,则修改为新的值
set:function(key,value,options){
//attrs变量中记录需要设置的数据对象
varattrs,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)
returnthis;
//如果被设置的数据对象属于Model类的一个实例,则将Model对象的attributes数据对象赋给attrs
//一般在复制一个Model对象的数据到另一个Model对象时,会执行该动作
if(attrsinstanceofModel)
attrs=attrs.attributes;
//如果options配置对象中设置了unset属性,则将attrs数据对象中的所有属性重置为undefined
//一般在复制一个Model对象的数据到另一个Model对象时,但仅仅需要复制Model中的数据而不需要复制值时执行该操作
if(options.unset)
for(attrinattrs)
attrs[attr]=
void0;
//对当前数据进行验证,如果验证未通过则停止执行
if(!this._validate(attrs,options))
returnfalse;
//如果设置的id属性名被包含在数据集合中,则将id覆盖到模型的id属性
//这是为了确保在自定义id属性名后,访问模型的id属性时,也能正确访问到id
if(this.idAttributeinattrs)
this.id=attrs[this.idAttribute];
varchanges=options.changes={};
//now记录当前模型中的数据对象
varnow=this.attributes;
//escaped记录当前模型中通过escape缓存过的数据
varescaped=this._escapedAttributes;
//prev记录模型中数据被改变之前的值
varprev=this._previousAttributes||{};
//遍历需要设置的数据对象
for(attrinattrs){
//attr存储当前属性名称,val存储当前属性的值
val=attrs[attr];
//如果当前数据在模型中不存在,或已经发生变化,或在options中指定了unset属性删除,则删除该数据被换存在_escapedAttributes中的数据
if(!_.isEqual(now[attr],val)||(options.unset&&_.has(now,attr))){
//仅删除通过escape缓存过的数据,这是为了保证缓存中的数据与模型中的真实数据保持同步
deleteescaped[attr];
//如果指定了silent属性,则此次set方法调用不会触发change事件,因此将被改变的数据记录到_silent属性中,便于下一次触发change事件时,通知事件监听函数此数据已经改变
//如果没有指定silent属性,则直接设置changes属性中当前数据为已改变状态
(options.silent?this._silent:changes)[attr]=true;
}
//如果在options中设置了unset,则从模型中删除该数据(包括key)
//如果没有指定unset属性,则认为将新增或修改数据,向模型的数据对象中加入新的数据
options.unset?
deletenow[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属性中移除已变化状态
deletethis.changed[attr];
deletethis._pending[attr];
}
}
//调用change方法,将触发change事件绑定的函数
if(!options.silent)
this.change(options);
returnthis;
},
//从当前模型中删除指定的数据(属性也将被同时删除)
unset:function(attr,options){
(options||(options={})).unset=true;
//通过options.unset配置项告知set方法进行删除操作
returnthis.set(attr,null,options);
},
//清除当前模型中的所有数据和属性
clear:function(options){
(options||(options={})).unset=true;
//克隆一个当前模型的属性副本,并通过options.unset配置项告知set方法执行删除操作
returnthis.set(_.clone(this.attributes),options);
},
//从服务器获取默认的模型数据,获取数据后使用set方法将数据填充到模型,因此如果获取到的数据与当前模型中的数据不一致,将会触发change事件
fetch:function(options){
//确保options是一个新的对象,随后将改变options中的属性
options=options?_.clone(options):{};
varmodel=this;
//在options中可以指定获取数据成功后的自定义回调函数
varsuccess=options.success;
//当获取数据成功后填充数据并调用自定义成功回调函数
options.success=function(resp,status,xhr){
//通过parse方法将服务器返回的数据进行转换
//通过set方法将转换后的数据填充到模型中,因此可能会触发change事件(当数据发生变化时)
//如果填充数据时验证失败,则不会调用自定义success回调函数
if(!model.set(model.parse(resp,xhr),options))
returnfalse;
//调用自定义的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存储需要保存到服务器的数据对象
varattrs,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))
returnfalse;
//记录当前模型中的数据,用于在将数据发送到服务器后,将数据进行还原
//如果服务器响应失败或没有返回数据,则可以保持修改前的状态
current=_.clone(this.attributes);
}
//silentOptions在options对象中加入了silent(不对数据进行验证)
//当使用wait参数时使用silentOptions配置项,因为在上面已经对数据进行过验证
//如果没有设置wait参数,则仍然使用原始的options配置项
varsilentOptions=_.extend({},options,{
silent:true
});
//将修改过最新的数据保存到模型中,便于在sync方法中获取模型数据保存到服务器
if(attrs&&!this.set(attrs,options.wait?silentOptions:options)){
returnfalse;
}
varmodel=this;
//在options中可以指定保存数据成功后的自定义回调函数
varsuccess=options.success;
//服务器响应成功后执行success
options.success=function(resp,status,xhr){
//获取服务器响应最新状态的数据
varserverAttrs=model.parse(resp,xhr);
//如果使用了wait参数,则优先将修改后的数据状态直接设置到模型
if(options.wait){
deleteoptions.wait;
serverAttrs=_.extend(attrs||{},serverAttrs);
}
//将最新的数据状态设置到模型中
//如果调用set方法时验证失败,则不会调用自定义的success回调函数
if(!model.set(serverAttrs,options))
returnfalse;
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方法(修改)
varmethod=this.isNew()?'create':'update';
varxhr=(this.sync||Backbone.sync).call(this,method,this,options);
//如果设置了options.wait,则将数据还原为修改前的状态
//此时保存的请求还没有得到响应,因此如果响应失败,模型中将保持修改前的状态,如果服务器响应成功,则会在success中设置模型中的数据为最新状态
if(options.wait)
this.set(current,silentOptions);
returnxhr;
},
//删除模型,模型将同时从所属的Collection集合中被删除
//如果模型是在客户端新建的,则直接从客户端删除
//如果模型数据同时存在服务器,则同时会删除服务器端的数据
destroy:function(options){
//配置项必须是一个新的对象
options=options?_.clone(options):{};
varmodel=this;
//在options中可以指定删除数据成功后的自定义回调函数
varsuccess=options.success;
//删除数据成功调用,触发destroy事件,如果模型存在于Collection集合中,集合将监听destroy事件并在触发时从集合中移除该模型
//删除模型时,模型中的数据并没有被清空,但模型已经从集合中移除,因此当没有任何地方引用该模型时,会被自动从内存中释放
//建议在删除模型时,将模型对象的引用变量设置为null
vartriggerDestroy=function(){
model.trigger('destroy',model,model.collection,options);
};
//如果该模型是一个客户端新建的模型,则直接调用triggerDestroy从集合中将模型移除
if(this.isNew()){
triggerDestroy();
returnfalse;
}
//当从服务器删除数据成功时
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方法发送删除数据的请求
varxhr=(this.sync||Backbone.sync).call(this,'delete',this,options);
//如果没有在options对象中配置wait项,则会先删除本地数据,再发送请求删除服务器数据
//此时无论服务器删除是否成功,本地模型数据已被删除
if(!options.wait)
triggerDestroy();
returnxhr;
},
//获取模型在服务器接口中对应的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路径
varbase=getValue(this,'urlRoot')||getValue(this.collection,'url')||urlError();
//如果当前模型是客户端新建的模型,则不存在id属性,服务器url直接使用base
if(this.isNew())
returnbase;
//如果当前模型具有id属性,可能是调用了save或destroy方法,将在base后面追加模型的id
//下面将判断base最后一个字符是否是"/",生成的url格式为[base/id]
returnbase+(base.charAt(base.length-1)=='/'?'':'/')+encodeURIComponent(this.id);
},
//parse方法用于解析从服务器获取的数据,返回一个能够被set方法解析的模型数据
//一般parse方法会根据服务器返回的数据进行重载,以便构建与服务器的无缝连接
//当服务器返回的数据结构与set方法所需的数据结构不一致(例如服务器返回XML格式数据时),可使用parse方法进行转换
parse:function(resp,xhr){
returnresp;
},
//创建一个新的模型,它具有和当前模型相同的数据
clone:function(){
returnnewthis.constructor(this.attributes);
},
//检查当前模型是否是客户端创建的新模型
//检查方式是根据模型是否存在id标识,客户端创建的新模型没有id标识
//因此服务器响应的模型数据中必须包含id标识,标识的属性名默认为"id",也可以通过修改idAttribute属性自定义标识
isNew:function(){
returnthis.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
varchanging=this._changing;
//开始执行标识,执行过程中值始终为true,执行完毕后this._changing被修改为false
this._changing=true;
//将非本次改变的数据状态添加到_pending对象中
for(varattrinthis._silent)
this._pending[attr]=true;
//changes对象包含了当前数据上一次执行change事件至今,已被改变的所有数据
//如果之前使用silent未触发change事件,则本次会被放到changes对象中
varchanges=_.extend({},options.changes,this._silent);
//重置_silent对象
this._silent={};
//遍历changes对象,分别针对每一个属性触发单独的change事件
for(varattrinchanges){
//将Model对象,属性值,配置项作为参数以此传递给事件的监听函数
this.trigger('change:'+attr,this,this.get(attr),options);
}
//如果方法处于执行中,则停止执行
if(changing)
returnthis;
//触发change事件,任意数据被改变后,都会依次触发"change:属性"事件和"change"事件
while(!_.isEmpty(this._pending)){
this._pending={};
//触发change事件,并将Model实例和配置项作为参数传递给监听函数
this.trigger('change',this,options);
//遍历changed对象中的数据,并依次将已改变数据的状态从changed中移除
//在此之后如果调用hasChanged检查数据状态,将得到false(未改变)
for(varattrinthis.changed){
if(this._pending[attr]||this._silent[attr])
continue;
//移除changed中数据的状态
deletethis.changed[attr];
}
//change事件执行完毕,_previousAttributes属性将记录当前模型最新的数据副本
//因此如果需要获取数据的上一个状态,一般只通过在触发的change事件中通过previous或previousAttributes方法获取
this._previousAttributes=_.clone(this.attributes);
}
//执行完毕标识
this._changing=false;
returnthis;
},
//检查某个数据是否在上一次执行change事件后被改变过
/**
*一般在change事件中配合previous或previousAttributes方法使用,如:
*if(model.hasChanged('attr')){
*varattrPrev=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)
returnthis.hasChanged()?_.clone(this.changed):false;
//指定了需要进行比较的diff集合,将返回上一次的数据与diff集合的比较结果
//old变量存储了上一个状态的模型数据
varval,changed=false,old=this._previousAttributes;
//遍历diff集合,并将每一项与上一个状态的集合进行比较
for(varattrindiff){
//将比较结果不一致的数据临时存储到changed变量
if(_.isEqual(old[attr],(val=diff[attr])))
continue;
(changed||(changed={}))[attr]=val;
}
//返回比较结果
returnchanged;
},
//在模型触发的change事件中,获取某个属性被改变前上一个状态的数据,一般用于进行数据比较或回滚
//该方法一般在change事件中调用,change事件被触发后,_previousAttributes属性存放最新的数据
previous:function(attr){
//attr指定需要获取上一个状态的属性名称
if(!arguments.length||!this._previousAttributes)
returnnull;
returnthis._previousAttributes[attr];
},
//在模型触发change事件中,获取所有属性上一个状态的数据集合
//该方法类似于previous()方法,一般在change事件中调用,用于数据比较或回滚
previousAttributes:function(){
//将上一个状态的数据对象克隆为一个新对象并返回
return_.clone(this._previousAttributes);
},
//Checkifthemodeliscurrentlyinavalidstate.It'sonlypossibleto
//getintoan*invalid*stateifyou'reusingsilentchanges.
//验证当前模型中的数据是否能通过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)
returntrue;
//获取对象中所有的属性值,并放入validate方法中进行验证
//validate方法包含2个参数,分别为模型中的数据集合与配置对象,如果验证通过则不返回任何数据(默认为undefined),验证失败则返回带有错误信息数据
attrs=_.extend({},this.attributes,attrs);
varerror=this.validate(attrs,options);
//验证通过
if(!error)
returntrue;
//验证未通过
//如果配置对象中设置了error错误处理方法,则调用该方法并将错误数据和配置对象传递给该方法
if(options&&options.error){
options.error(this,error,options);
}else{
//如果对模型绑定了error事件监听,则触发绑定事件
this.trigger('error',this,error,options);
}
//返回验证未通过标识
returnfalse;
}
});
//Backbone.Collection数据模型集合相关
//-------------------
//Collection集合存储一系列相同类的数据模型,并提供相关方法对模型进行操作
varCollection=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结果组成一个数组,并返回
returnthis.map(function(model){
//依次调用每个模型对象的toJSON方法,该方法默认将返回模型的数据对象(复制的副本)
//如果需要返回字符串等其它形式,可以重载toJSON方法
returnmodel.toJSON(options);
});
},
//向集合中添加一个或多个模型对象
//默认会触发"add"事件,如果在options中设置了silent属性,可以关闭此次事件触发
//传入的models可以是一个或一系列的模型对象(Model类的实例),如果在集合中设置了model属性,则允许直接传入数据对象(如{name:'test'}),将自动将数据对象实例化为model指向的模型对象
add:function(models,options){
//局部变量定义
vari,index,length,model,cid,id,cids={},ids={},dups=[];
options||(options={});
//models必须是一个数组,如果只传入了一个模型,则将其转换为数组
models=_.isArray(models)?models.slice():[models];
//遍历需要添加的模型列表,遍历过程中,将执行以下操作:
//-将数据对象转化模型对象
//-建立模型与集合之间的引用
//-记录无效和重复的模型,并在后面进行过滤
for(i=0,length=models.length;i<length;i++){
//将数据对象转换为模型对象,简历模型与集合的引用,并存储到model(同时models中对应的模型已经被替换为模型对象)
if(!(model=models[i]=this._prepareModel(models[i],options))){
thrownewError("Can'taddaninvalidmodeltoacollection");
}
//当前模型的cid和id
cid=model.cid;
id=model.id;
//dups数组中记录了无效或重复的模型索引(models数组中的索引),并在下一步进行过滤删除
//如果cids,ids变量中已经存在了该模型的索引,则认为是同一个模型在传入的models数组中声明了多次
//如果_byCid,_byId对象中已经存在了该模型的索引,则认为同一个模型在当前集合中已经存在
//对于上述两种情况,将模型的索引记录到dups进行过滤删除
if(cids[cid]||this._byCid[cid]||((id!=null)&&(ids[id]||this._byId[id]))){
dups.push(i);
continue;
}
//将models中已经遍历过的模型记录下来,用于在下一次循环时进行重复检查
cids[cid]=ids[id]=model;
}
//从models中删除无效或重复的模型,保留目前集合中真正需要添加的模型列表
i=dups.length;
while(i--){
models.splice(dups[i],1);
}
//遍历需要添加的模型,监听模型事件并记录_byCid,_byId列表,用于在调用get和getByCid方法时作为索引
for(i=0,length=models.length;i<length;i++){
//监听模型中的所有事件,并执行_onModelEvent方法
//_onModelEvent方法中会对模型抛出的add,remove,destroy和change事件进行处理,以便模型与集合中的状态保持同步
(model=models[i]).on('all',this._onModelEvent,this);
//将模型根据cid记录到_byCid对象,便于根据cid进行查找
this._byCid[model.cid]=model;
//将模型根据id记录到_byId对象,便于根据id进行查找
if(model.id!=null)
this._byId[model.id]=model;
}
//改变集合的length属性,length属性记录了当前集合中模型的数量
this.length+=length;
//设置新模型列表插入到集合中的位置,如果在options中设置了at参数,则在集合的at位置插入
//默认将插入到集合的末尾
//如果设置了comparator自定义排序方法,则设置at后还将按照comparator中的方法进行排序,因此最终的顺序可能并非在at指定的位置
index=options.at!=null?options.at:this.models.length;
splice.apply(this.models,[index,0].concat(models));
//如果设置了comparator方法,则将数据按照comparator中的算法进行排序
//自动排序使用silent属性阻止触发reset事件
if(this.comparator)
this.sort({
silent:true
});
//依次对每个模型对象触发"add"事件,如果设置了silent属性,则阻止事件触发
if(options.silent)
returnthis;
//遍历新增加的模型列表
for(i=0,length=this.models.length;i<length;i++){
if(!cids[(model=this.models[i]).cid])
continue;
options.index=i;
//触发模型的"add"事件,因为集合监听了模型的"all"事件,因此在_onModelEvent方法中,集合也将触发"add"事件
//详细信息可参考Collection.prototype._onModelEvent方法
model.trigger('add',model,this,options);
}
returnthis;
},
//从集合中移除模型对象(支持移除多个模型)
//传入的models可以是需要移除的模型对象,或模型的cid和模型的id
//移除模型并不会调用模型的destroy方法
//如果没有设置options.silent参数,将触发模型的remove事件,同时将触发集合的remove事件(集合通过_onModelEvent方法监听了模型的所有事件)
remove:function(models,options){
vari,l,index,model;
//options默认为空对象
options||(options={});
//models必须是数组类型,当只移除一个模型时,将其放入一个数组
models=_.isArray(models)?models.slice():[models];
//遍历需要移除的模型列表
for(i=0,l=models.length;i<l;i++){
//所传入的models列表中可以是需要移除的模型对象,或模型的cid和模型的id
//(在getByCid和get方法中,可通过cid,id来获取模型,如果传入的是一个模型对象,则返回模型本身)
model=this.getByCid(models[i])||this.get(models[i]);
//没有获取到模型
if(!model)
continue;
//从_byId列表中移除模型的id引用
deletethis._byId[model.id];
//从_byCid列表中移除模型的cid引用
deletethis._byCid[model.cid];
//indexOf是Underscore对象中的方法,这里通过indexOf方法获取模型在集合中首次出现的位置
index=this.indexOf(model);
//从集合列表中移除该模型
this.models.splice(index,1);
//重置当前集合的length属性(记录集合中模型的数量)
this.length--;
//如果没有设置silent属性,则触发模型的remove事件
if(!options.silent){
//将当前模型在集合中的位置添加到options对象并传递给remove监听事件,以便在事件函数中可以使用
options.index=index;
model.trigger('remove',model,this,options);
}
//解除模型与集合的关系,包括集合中对模型的引用和事件监听
this._removeReference(model);
}
returnthis;
},
//向集合的末尾添加模型对象
//如果集合类中定义了comparator排序方法,则通过push方法添加的模型将按照comparator定义的算法进行排序,因此模型顺序可能会被改变
push:function(model,options){
//通过_prepareModel方法将model实例化为模型对象,这句代码是多余的,因为在下面调用的add方法中还会通过_prepareModel获取一次模型
model=this._prepareModel(model,options);
//调用add方法将模型添加到集合中(默认添加到集合末尾)
this.add(model,options);
returnmodel;
},
//移除集合中最后一个模型对象
pop:function(options){
//获取集合中最后一个模型
varmodel=this.at(this.length-1);
//通过remove方法移除该模型
this.remove(model,options);
returnmodel;
},
//向集合的第一个位置插入模型
//如果集合类中定义了comparator排序方法,则通过unshift方法添加的模型将按照comparator定义的算法进行排序,因此模型顺序可能会被改变
unshift:function(model,options){
//通过_prepareModel方法将model实例化为模型对象
model=this._prepareModel(model,options);
//调用add方法将模型插入到集合的第一个位置(设置at为0)
//如果定义了comparator排序方法,集合的顺序将被重排
this.add(model,_.extend({
at:0
},options));
returnmodel;
},
//移除并返回集合中的第一个模型对象
shift:function(options){
//获得集合中的第一个模型
varmodel=this.at(0);
//从集合中删除该模型
this.remove(model,options);
//返回模型对象
returnmodel;
},
//根据id从集合中查找模型并返回
get:function(id){
if(id==null)
return
void0;
returnthis._byId[id.id!=null?id.id:id];
},
//根据cid从集合中查找模型并返回
getByCid:function(cid){
returncid&&this._byCid[cid.cid||cid];
},
//根据索引(下标,从0开始)从集合中查找模型并返回
at:function(index){
returnthis.models[index];
},
//对集合中的模型根据值进行筛选
//attrs是一个筛选对象,如{name:'Jack'},将返回集合中所有name为"Jack"的模型(数组)
where:function(attrs){
//attrs不能为空值
if(_.isEmpty(attrs))
return[];
//通过filter方法对集合中的模型进行筛选
//filter方法是Underscore中的方法,用于将遍历集合中的元素,并将能通过处理器验证(返回值为true)的元素作为数组返回
returnthis.filter(function(model){
//遍历attrs对象中的验证规则
for(varkeyinattrs){
//将attrs中的验证规则与集合中的模型进行匹配
if(attrs[key]!==model.get(key))
returnfalse;
}
returntrue;
});
},
//对集合中的模型按照comparator属性指定的方法进行排序
//如果没有在options中设置silent参数,则排序后将触发reset事件
sort:function(options){
//options默认是一个对象
options||(options={});
//调用sort方法必须指定了comparator属性(排序算法方法),否则将抛出一个错误
if(!this.comparator)
thrownewError('Cannotsortasetwithoutacomparator');
//boundComparator存储了绑定当前集合上下文对象的comparator排序算法方法
varboundComparator=_.bind(this.comparator,this);
if(this.comparator.length==1){
this.models=this.sortBy(boundComparator);
}else{
//调用Array.prototype.sort通过comparator算法对数据进行自定义排序
this.models.sort(boundComparator);
}
//如果没有指定silent参数,则触发reset事件
if(!options.silent)
this.trigger('reset',this,options);
returnthis;
},
//将集合中所有模型的attr属性值存放到一个数组并返回
pluck:function(attr){
//map是Underscore中的方法,用于遍历一个集合,并将所有处理器的返回值作为一个数组返回
return_.map(this.models,function(model){
//返回当前模型的attr属性值
returnmodel.get(attr);
});
},
//替换集合中的所有模型数据(models)
//该操作将删除集合中当前的所有数据和状态,并重新将数据设置为models
//models应该是一个数组,可以包含一系列Model模型对象,或原始对象(将在add方法中自动创建为模型对象)
reset:function(models,options){
//models是进行替换的模型(或数据)数组
models||(models=[]);
//options默认是一个空对象
options||(options={});
//遍历当前集合中的模型,依次删除并解除它们与集合的引用关系
for(vari=0,l=this.models.length;i<l;i++){
this._removeReference(this.models[i]);
}
//删除集合数据并重置状态
this._reset();
//通过add方法将新的模型数据添加到集合
//这里通过exnted方法将配置项覆盖到一个新的对象,该对象默认silent为true,因此不会触发"add"事件
//如果在调用reset方法时没有设置silent属性则会触发reset事件,如果设置为true则不会触发任何事件,如果设置为false,将依次触发"add"和"reset"事件
this.add(models,_.extend({
silent:true
},options));
//如果在调用reset方法时没有设置silent属性,则触发reset事件
if(!options.silent)
this.trigger('reset',this,options);
returnthis;
},
//从服务器获取集合的初始化数据
//如果在options中设置参数add=true,则获取到的数据会被追加到集合中,否则将以服务器返回的数据替换集合中的当前数据
fetch:function(options){
//复制options对象,因为options对象在后面会被修改用于临时存储数据
options=options?_.clone(options):{};
if(options.parse===undefined)
options.parse=true;
//collection记录当前集合对象,用于在success回调函数中使用
varcollection=this;
//自定义回调函数,数据请求成功后并添加完成后,会调用自定义success函数
varsuccess=options.success;
//当从服务器请求数据成功时执行options.success,该函数中将解析并添加数据
options.success=function(resp,status,xhr){
//通过parse方法对服务器返回的数据进行解析,如果需要自定义数据结构,可以重载parse方法
//如果在options中设置add=true,则调用add方法将数据添加到集合,否则将通过reset方法将集合中的数据替换为服务器的返回数据
collection[options.add?'add':'reset'](collection.parse(resp,xhr),options);
//如果设置了自定义成功回调,则执行
if(success)
success(collection,resp);
};
//当服务器返回状态错误时,通过wrapError方法处理错误事件
options.error=Backbone.wrapError(options.error,collection,options);
//调用Backbone.sync方法发送请求从服务器获取数据
//如果需要的数据并不是从服务器获取,或获取方式不使用AJAX,可以重载Backbone.sync方法
return(this.sync||Backbone.sync).call(this,'read',this,options);
},
//向集合中添加并创建一个模型,同时将该模型保存到服务器
//如果是通过数据对象来创建模型,需要在集合中声明model属性对应的模型类
//如果在options中声明了wait属性,则会在服务器创建成功后再将模型添加到集合,否则先将模型添加到集合,再保存到服务器(无论保存是否成功)
create:function(model,options){
varcoll=this;
//定义options对象
options=options?_.clone(options):{};
//通过_prepareModel获取模型类的实例
model=this._prepareModel(model,options);
//模型创建失败
if(!model)
returnfalse;
//如果没有声明wait属性,则通过add方法将模型添加到集合中
if(!options.wait)
coll.add(model,options);
//success存储保存到服务器成功之后的自定义回调函数(通过options.success声明)
varsuccess=options.success;
//监听模型数据保存成功后的回调函数
options.success=function(nextModel,resp,xhr){
//如果声明了wait属性,则在只有在服务器保存成功后才会将模型添加到集合中
if(options.wait)
coll.add(nextModel,options);
//如果声明了自定义成功回调,则执行自定义函数,否则将默认触发模型的sync事件
if(success){
success(nextModel,resp);
}else{
nextModel.trigger('sync',model,resp,options);
}
};
//调用模型的save方法,将模型数据保存到服务器
model.save(null,options);
returnmodel;
},
//数据解析方法,用于将服务器数据解析为模型和集合可用的结构化数据
//默认将返回resp本身,这需要与服务器定义Backbone支持的数据格式,如果需要自定义数据格式,可以重载parse方法
parse:function(resp,xhr){
returnresp;
},
//chain用于构建集合数据的链式操作,它将集合中的数据转换为一个Underscore对象,并使用Underscore的chain方法转换为链式结构
//关于chain方法的转换方式,可参考Underscore中chain方法的注释
chain:function(){
return_(this.models).chain();
},
//删除所有集合元素并重置集合中的数据状态
_reset:function(options){
//删除集合元素
this.length=0;
this.models=[];
//重置集合状态
this._byId={};
this._byCid={};
},
//将模型添加到集合中之前的一些准备工作
//包括将数据实例化为一个模型对象,和将集合引用到模型的collection属性
_prepareModel:function(model,options){
options||(options={});
//检查model是否是一个模型对象(即Model类的实例)
if(!(modelinstanceofModel)){
//传入的model是模型数据对象,而并非模型对象
//将数据作为参数传递给Model,以创建一个新的模型对象
varattrs=model;
//设置模型引用的集合
options.collection=this;
//将数据转化为模型
model=newthis.model(attrs,options);
//对模型中的数据进行验证
if(!model._validate(model.attributes,options))
model=false;
}elseif(!model.collection){
//如果传入的是一个模型对象但没有建立与集合的引用,则设置模型的collection属性为当前集合
model.collection=this;
}
returnmodel;
},
//解绑某个模型与集合的关系,包括对集合的引用和事件监听
//一般在调用remove方法删除模型或调用reset方法重置状态时自动调用
_removeReference:function(model){
//如果模型引用了当前集合,则移除该引用(必须确保所有对模型的引用已经解除,否则模型可能无法从内存中释放)
if(this==model.collection){
deletemodel.collection;
}
//取消集合中监听的所有模型事件
model.off('all',this._onModelEvent,this);
},
//在向集合中添加模型时被自动调用
//用于监听集合中模型的事件,当模型在触发事件(add,remove,destroy,change事件)时集合进行相关处理
_onModelEvent:function(event,model,collection,options){
//添加和移除模型的事件,必须确保模型所属的集合为当前集合对象
if((event=='add'||event=='remove')&&collection!=this)
return;
//模型触发销毁事件时,从集合中移除
if(event=='destroy'){
this.remove(model,options);
}
//当模型的id被修改时,集合修改_byId中存储对模型的引用,保持与模型id的同步,便于使用get()方法获取模型对象
if(model&&event==='change:'+model.idAttribute){
//获取模型在改变之前的id,并根据此id从集合的_byId列表中移除
deletethis._byId[model.previous(model.idAttribute)];
//以模型新的id作为key,在_byId列表中存放对模型的引用
this._byId[model.id]=model;
}
//在集合中触发模型对应的事件,无论模型触发任何事件,集合都会触发对应的事件
//(例如当模型被添加到集合中时,会触发模型的"add"事件,同时也会在此方法中触发集合的"add"事件)
//这对于监听并处理集合中模型状态的变化非常有效
//在监听的集合事件中,触发对应事件的模型会被作为参数传递给集合的监听函数
this.trigger.apply(this,arguments);
}
});
//定义Underscore中的集合操作的相关方法
//将Underscore中一系列集合