事件模型及其原理
Backbone.Events就是事件实现的核心,它可以让对象拥有事件能力
var Events = Backbone.Events = { .. }
对象通过listenTo侦听其他对象,通过trigger触发事件。可以脱离Backbone的MVC,在自定义的对象上使用事件
var model = _.extend({},Backbone.Events);
var view = _.extend({},Backbone.Events);
view.listenTo(model,\'custom_event\',function(){ alert(\'catch the event\') });
model.trigger(\'custom_event\');
执行结果:
Backbone的Model和View等核心类,都是继承自Backbone.Events的。例如Backbone.Model:
var Events = Backbone.Events = { .. }
var Model = Backbone.Model = function(attributes, options) {
...
};
_.extend(Model.prototype, Events, { ... })
从原理上讲,事件是这么工作的:
被侦听的对象维护一个事件数组_event,其他对象在调用listenTo时,会将事件名与回调维护到队列中:
一个事件名可以对应多个回调,对于被侦听者而言,只知道回调的存在,并不知道具体是哪个对象在侦听它。当被侦听者调用trigger(name)时,会遍历_event,选择同名的事件,并将其下面所有的回调都执行一遍。
需要额外注意的是,Backbone的listenTo实现,除了使被侦听者维护对侦听者的引用外,还使侦听者也维护了被侦听者。这是为了在恰当的时候,侦听者可以单方面中断侦听。因此,虽然是循环引用,但是使用Backbone的合适的方法可以很好的维护,不会有问题,在后面的内存泄露部分将看到。
另外,有时只希望事件在绑定后,当回调发生后,就接触绑定。这在一些对公共模块的引用时很有用。listenToOnce可以做到这一点
与服务器同步数据
backbone默认实现了一套与RESTful风格的服务端同步模型的机制,这套机制不仅可以减轻开发人员的工作量,而且可以使模型变得更为健壮(在各种异常下仍能保持数据一致性)。不过,要真正发挥这个功效,一个与之匹配的服务端实现是很重要的。为了说明问题,假设服务端有如下REST风格的接口:
backbone会使用到上面这些HTTP方法的地方主要有以下几个:
options参数存在于上面任何一个方法的参数列表中,通过options可以修改backbone和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属性。Collection沿用了underscore90%的集合和数组操作,使得集合操作极其方便:
// 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属性可以通过五种途径给出,优先级从高到低:
究竟如何选择,取决于以下几点:
视图类还有几个属性可以导出,由外部初始化,它们是:
// 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,把视图对象可能的被外部的侦听也断开。
事件驱动模块
自定义事件:自定义事件比较适合多人合作开发,因为我们知道,函数名如果一样的话,那么后面的函数会覆盖前面的,而事件在绑定的情况下是不会被覆盖的。
<script type=\"text/javascript\">
//自定义事件
var Mod = backbone.Model.extend({
defaults : {
name : \'trigkit4\';
},
initialization : function(){ //初始化构造函数
this.on(\'change\',function(){ //绑定change事件,当数据改变时执行此回调函数
alert(123);
});
}
});
var model = new Mod;
model.set(\'name\' ,\'backbone\');//修改默认的name属性值为backbone,此时数据被改变,弹出123
</script>
事件绑定
除此之外,我们还可以自定义要绑定的被改变的数据类型:
object.on(event, callback, [context])
绑定一个回调函数到一个对象上, 当事件触发时执行回调函数 :
<script type=\"text/javascript\">
//自定义事件
var Mod = backbone.Model.extend({
defaults : {
name : \'trigkit4\',
age : 21;
},
initialization : function(){ //初始化构造函数
this.on(\'change:age\',function(){ //绑定change事件,当数据改变时执行此回调函数
alert(123);
});
}
});
var model = new Mod;
model.set(\'name\' ,\'backbone\');//修改默认的name属性值为backbone,此时数据被改变,弹出123
</script>
listenTo
<script type=\"text/javascript\">
$(function(){
var Mod = Backbone.Model.extend({
defaults : {
name : \'trigkit4\'
}
});
var V = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model,\'change\',this.show);//listenTo比on多了个参数
},
show : function(model){
$(\'body\').append(\'<div>\' + model.get(\'name\') + \'</div>\');
}
});
var m = new Mod;
var v = new V({model:m});//model指定创建的模型对象m,即前面的路由,哈希值的对应
m.set(\'name\',\'hello\');//对模型进行就改时,触发事件,页面也就更新了
});
</script>
istenTo
<script type=\"text/javascript\">
$(function(){
var Mod = Backbone.Model.extend({
defaults : {
name : \'trigkit4\'
}
});
var V = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model,\'change\',this.show);//listenTo比on多了个参数
},
show : function(model){
$(\'body\').append(\'<div>\' + model.get(\'name\') + \'</div>\');
}
});
var m = new Mod;
var v = new V({model:m});//model指定创建的模型对象m,即前面的路由,哈希值的对应
m.set(\'name\',\'hello\');//对模型进行就改时,触发事件,页面也就更新了
});
</script>
模型集合器
Backbone.Collection
集合是模型的有序组合,我们可以在集合上绑定 \"change\" 事件,从而当集合中的模型发生变化时获得通知,集合也可以监听 \"add\" 和 “remove\" 事件, 从服务器更新,并能使用 Underscore.js 提供的方法
路由与历史管理
<script type=\"text/javascript\">
var Workspace = Backbone.Router.extend({
routes: {
\"help\": \"help\",
\"search/:query\": \"search\",
\"search/:query/p:page\":\" search\"
},
help : function(){
alert(123);
},
search : function(query,page){
alert(345);
}
});
var w = new Workspace;
Backbone.history.start();//backbone通过hash值找到对应的回调函数
</script>
事件委托
<script type=\"text/javascript\">
$(function(){
var V = Backbone.View.extend({
el : $(\'body\'),
//对events进行集体操作
events : {
\"click input\" : \"hello\",
\"mouseover li\" : \"world\"
},
hello : function(){
alert(1234);
},
world : function(){
alert(123)
}
});
var view = new V;
});
</script>
<body>
<imput type = \"button\" value = \"hwx\" />
<ul>
<li>1234</li>
<li>1234</li>
<li>1234</li>
<li>1234</li>
<li>1234</li>
</ul>
</body>
事件委托 格式:事件 + 空格 + 由谁来触发 : 对应的回调函数
本文地址:https://www.stayed.cn/item/2024
转载请注明出处。
本站部分内容来源于网络,如侵犯到您的权益,请 联系我