事件模型及其原理
Backbone.Events
就是事件实现的核心,它可让对象拥有事件能力html
var Events = Backbone.Events = { .. }
对象经过listenTo
侦听其余对象,经过trigger
触发事件。能够脱离Backbone的MVC,在自定义的对象上使用事件java
var model = _.extend({},Backbone.Events); var view = _.extend({},Backbone.Events); view.listenTo(model,'custom_event',function(){ alert('catch the event') }); model.trigger('custom_event');
执行结果:jquery
Backbone的Model和View等核心类,都是继承自Backbone.Events
的。例如Backbone.Model:ajax
var Events = Backbone.Events = { .. }
var Model = Backbone.Model = function(attributes, options) { ... }; _.extend(Model.prototype, Events, { ... })
从原理上讲,事件是这么工做的:api
被侦听的对象维护一个事件数组_event
,其余对象在调用listenTo
时,会将事件名与回调维护到队列中:数组
一个事件名能够对应多个回调,对于被侦听者而言,只知道回调的存在,并不知道具体是哪一个对象在侦听它。当被侦听者调用trigger(name)
时,会遍历_event,选择同名的事件,并将其下面全部的回调都执行一遍。服务器
须要额外注意的是,Backbone的listenTo
实现,除了使被侦听者维护对侦听者的引用外,还使侦听者也维护了被侦听者。这是为了在恰当的时候,侦听者能够单方面中断侦听。所以,虽然是循环引用,可是使用Backbone的合适的方法能够很好的维护,不会有问题,在后面的内存泄露部分将看到。app
另外,有时只但愿事件在绑定后,当回调发生后,就接触绑定。这在一些对公共模块的引用时颇有用。listenToOnce
能够作到这一点异步
与服务器同步数据
backbone
默认实现了一套与RESTful风格的服务端同步模型的机制,这套机制不只能够减轻开发人员的工做量,并且可使模型变得更为健壮(在各类异常下仍能保持数据一致性)。不过,要真正发挥这个功效,一个与之匹配的服务端实现是很重要的。为了说明问题,假设服务端有以下REST风格的接口:
- GET
/resources
获取资源列表 - POST
/resources
建立一个资源,返回资源的所有或部分字段 - GET
/resources/{id}
获取某个id的资源详情,返回资源的所有或部分字段 - DELETE
/resources/{id}
删除某个资源 - PUT
/resources/{id}
更新某个资源的所有
字段,返回资源的所有或部分字段 - PATCH
/resources/{id}
更新某个资源的部分
字段,返回资源的所有或部分字段
backbone
会使用到上面这些HTTP方法的地方主要有如下几个:
Model.save()
逻辑上,根据当前这个model的是否具备id
来判断应该使用POST仍是PUT,若是model没有id,表示是新的模型,将使用POST
,将模型的字段所有提交到/resources
;若是model具备id,表示是已经存在的模型,将使用PUT
,将模型的所有字段提交到/resources/{id}
。当传入options
包含patch:true
的时候,save会产生PATCH
。Model.destroy()
会产生DELETE
,目标url为/resources/{id}
,若是当前model不包含id时,不会与服务端同步,由于此时backbone认为model在服务端尚不存在,不须要删除Model.fetch()
会产生GET
,目标url为/resources/{id}
,并将得到的属性更新model。Collection.fetch()
会产生GET
,目标url为/resources
,并对返回的数组中的每一个对象,自动实例化modelCollection.create()
实际将调用Model.save
options
参数存在于上面任何一个方法的参数列表中,经过options
能够修改backbone和ajax请求的一些行为,可使用的options包括:
wait
: 能够指定是否等待服务端的返回结果再更新model。默认状况下不等待url
: 能够覆盖掉backbone默认使用的url格式attrs
: 能够指定保存到服务端的字段有哪些,配合options.patch
能够产生PATCH
对模型进行部分更新patch
: 指定使用部分更新的REST接口data
: 会被直接传递给jquery的ajax中的data,可以覆盖backbone全部的对上传的数据控制的行为其余
: options中的任何参数都将直接传递给jquery的ajax,做为其options
backbone经过Model的urlRoot
属性或者是Collection
的url
属性得知具体的服务端接口地址,以便发起ajax。在Model的url
默认实现中,Model除了会考察urlRoot
,第二选择会是Model所在Collection的url
,全部有时只须要在Collection里面书写url
就能够了。
Backbone会根据与服务端要进行什么类型的操做,决定是否要添加id
在url
后面,如下代码是Model的默认url
实现:
url: function () { var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); if (this.isNew()) return base; return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); },
其中的正则式/([^\/])$/
是个很巧妙的处理,它解决了url
最后是否包含'/'
的不肯定性。
这个正则匹配的是行末的非
/
字符,这样,像/resources
这样的目标会匹配s
,而后replace
中使用分组编号$1
捕获了s
,将s
替换为s/
,这样就自动加上了缺失的/
;而当/resources/
这样目标却没法匹配到结果,也就不须要替换了。
Model和Collection的关系
在backbone中,即使一类的模型实例的确是在一个集合里面,也并无强制要求使用集合类。可是使用集合有一些额外的好处,这些好处包括:
url继承
Model
属于Collection
后,能够继承Collection的url
属性。上面一节已经提到了
underscore集合能力
Collection
沿用了underscore
90%的集合和数组操做,使得集合操做极其方便:
// Underscore methods that we want to implement on the Collection. // 90% of the core usefulness of Backbone Collections is actually implemented // right here: var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
Backbone巧妙的使用下面的代码将这些方法附加到Collection
中:
// Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function (method) { Collection.prototype[method] = function () { var args = slice.call(arguments); //将参数数组转化成真正的数组 args.unshift(this.models); //将Collection真正用来维护集合的数组,做为第一个个参数 return _[method].apply(_, args); //使用apply调用underscore的方法 }; });
自动侦听和转发集合中的Model事件
集合可以自动侦听并转发集合中的元素的事件,还有一些事件集合会作相应的特殊处理,这些事件包括:
destroy
侦听到元素的destroy
事件后,会自动将元素从集合中移除,并引起remove
事件change:id
侦听到元素的id属性被change后,自动更新内部对model的引用关系
自动模型构造
利用Collection
的fetch
,能够加载服务端数据集合,与此同时,能够自动建立相关的Model实例,并调用构造方法
元素重复判断
Collection
会根据Model
的idAttribute
指定的惟一键,来判断元素是否重复,默认状况下惟一键是id
,能够重写idAttribute
来覆盖。当元素重复的时候,能够选择是丢弃重复元素,仍是合并两种元素,默认是丢弃的
模型转化
有时从REST接口获得的数据并不能彻底知足界面的处理需求,能够经过Model.parse
或者Collection.parse
方法,在实例化Backbone对象前,对数据进行预处理。大致上,Model.parse
用来对返回的单个对象进行属性的处理,而Collection.parse
用来对返回的集合进行处理,一般是过滤掉没必要要的数据。例如:
//只挑选type=1的book var Books = Backbone.Collection.extend({ parse:function(models,options){ return _.filter(models , function(model){ return model.type == 1; }) } }) //为Book对象添加url属性,以便渲染 var Book = Backbone.Model.extend({ parse: function(model,options){ return _.extend(model,{ url : '/books/' + model.id }); } })
经过Collection的fetch
,自动实例化的Model,其parse也会被调用。
模型的默认值
Model能够经过设置defaults
属性来设置默认值,这颇有用。由于,不管是模型仍是集合,fetch数据都是异步的,而每每视图的渲染确实极可能在数据到来前就进行了,若是没有默认值的话,一些使用了模板引擎的视图,在渲染的时候可能会出错。例如underscore自带的视图引擎,因为使用with(){}
语法,会由于对象缺少属性而报错。
视图的el
Backbone的视图对象十分简答,对于开发者而言,仅仅关心一个el属性便可。el属性能够经过五种途径给出,优先级从高到低:
- 实例化View的时候,传递el
- 在类中声明el
- 实例化View的时候传入
tagName
- 在类中声明
tagName
- 以上都没有的状况下使用默认的
'div'
究竟如何选择,取决于如下几点:
- 通常而言,若是模块是公用模块,在类中不提供el,而是让外部在实例化的时候传入,这样能够保持公共的View的独立性,不至于依赖已经存在的DOM元素
tagName
通常对于自成体系的View有用,好比table中的某行tr,ul中的某个li- 有些DOM事件必须在html存在的状况下才能绑定成功,好比
blur
,对于这种View,只能选择已经存在的html
视图类还有几个属性能够导出,由外部初始化,它们是:
// List of view options to be merged as properties. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
内存泄漏
事件机制能够很好的带来代码维护的便利,可是因为事件绑定会使对象之间的引用变得复杂和错乱,容易形成内存泄漏。下面的写法就会形成内存泄漏:
var Task = Backbone.Model.extend({}) var TaskView = Backbone.View.extend({ tagName: 'tr', template: _.template('<td><%= id %></td><td><%= summary %></td><td><%= description %></td>'), initialize: function(){ this.listenTo(this.model,'change',this.render); }, render: function(){ this.$el.html( this.template( this.model.toJSON() ) ); return this; } }) var TaskCollection = Backbone.Collection.extend({ url: 'http://api.test.clippererm.com/api/testtasks', model: Task, comparator: 'summary' }) var TaskCollectionView = Backbone.View.extend({ initialize: function(){ this.listenTo(this.collection, 'add',this.addOne); this.listenTo(this.collection, 'reset',this.render); }, addOne: function(task){ var view = new TaskView({ model : task }); this.$el.append(view.render().$el); }, render: function(){ var _this = this; //简单粗暴的将DOM清空 //在sort事件触发的render调用时,以前实例化的TaskView对象会泄漏 this.$el.empty(); this.collection.each(function(model){ _this.addOne(model); }) return this; } })
使用下面的测试代码,并结合Chrome的堆内存快照来证实:
var tasks = null; var tasklist = null; $(function () { // body... $('#start').click(function(){ tasks = new TaskCollection(); tasklist = new TaskCollectionView({ collection : tasks, el: '#tasklist' }) tasklist.render(); tasks.fetch(); }) $('#refresh').click(function(){ tasks.fetch({ reset : true }); }) $('#sort').click(function(){ //将侦听sort放在这里,避免第一次加载数据后的自动排序,触发的sort事件,以致于混淆 tasklist.listenToOnce(tasks,'sort',tasklist.render); tasks.sort(); }) })
点击开始,使用Chrome的’Profile’下的’Take Heap Snapshot’功能,查看当前堆内存状况,使用child
类型过滤,能够看到Backbone对象实例一共有10个(1+1+4+4):
之因此用child过滤,由于咱们的类继承自Backbone的类型,而继承使用了重写原型的方法,Backbone在继承时,使用的变量名为
child
,最后,child
被返回出来了
点击排序后,再次抓取快照,能够看到实例个数变成了14个,这是由于,在render
过程当中,又建立了4个新的TaskView
,而以前的4个TaskView
并无释放(之因此是4个是由于记录的条数是4)
再次点击排序,再次抓取快照,实例数又增长了4个,变成了18个!
那么,为何每次排序后,以前的TaskView
没法释放呢。由于TaskView的实例都会侦听model,致使model对新建立的TaskView的实例存在引用,因此旧的TaskView没法删除,又建立了新的,致使内存不断上涨。并且因为引用存在于change
事件的回调队列里,model每次触发change
都会通知旧的TaskView实例,致使执行不少无用的代码。那么如何改进呢?
修改TaskCollectionView:
var TaskCollectionView = Backbone.View.extend({ initialize: function(){ this.listenTo(this.collection, 'add',this.addOne); this.listenTo(this.collection, 'reset',this.render); //初始化一个view数组以跟踪建立的view this.views =[] }, addOne: function(task){ var view = new TaskView({ model : task }); this.$el.append(view.render().$el); //将新建立的view保存起来 this.views.push(view); }, render: function(){ var _this = this; //遍历views数组,并对每一个view调用Backbone的remove _.each(this.views,function(view){ view.remove().off(); }) //清空views数组,此时旧的view就变成没有任何被引用的不可达对象了 //垃圾回收器会回收它们 this.views =[]; this.$el.empty(); this.collection.each(function(model){ _this.addOne(model); }) return this; } })
Backbone的View有一个remove
方法,这个方法除了删除View所关联的DOM对象,还会阻断事件侦听,它经过在listenTo方法时记录下来的那些被侦听对象(上文事件原理中提到),来使这些被侦听的对象删除对本身的引用。在remove
内部使用事件基类的stopListening
完成这个动做。 上面的代码使用一个views数组来跟踪新建立的TaskView
对象,并在render的时候,依次调用这些视图对象的remove
,而后清空数组,这样这些TaskView
对象就能获得释放。而且,除了调用remove
,还调用了off
,把视图对象可能的被外部的侦听也断开。