转自  http://www.pchou.info/javascript/2014/06/26/backbone-summary-01.html

开始在项目中大规模使用backbone,一路磕磕碰碰,边做边学习边体会,有一些心得和体会,记录在本文中

 

事件模型及其原理

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风格的接口:

  • 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,并对返回的数组中的每个对象,自动实例化model
  • Collection.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属性或者是Collectionurl属性得知具体的服务端接口地址,以便发起ajax。在Model的url默认实现中,Model除了会考察urlRoot,第二选择会是Model所在Collection的url,所有有时只需要在Collection里面书写url就可以了。

Backbone会根据与服务端要进行什么类型的操作,决定是否要添加idurl后面,以下代码是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沿用了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的引用关系

自动模型构造

利用Collectionfetch,可以加载服务端数据集合,与此同时,可以自动创建相关的Model实例,并调用构造方法

元素重复判断

Collection会根据ModelidAttribute指定的唯一键,来判断元素是否重复,默认情况下唯一键是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属性可以通过五种途径给出,优先级从高到低:

  1. 实例化View的时候,传递el
  2. 在类中声明el
  3. 实例化View的时候传入tagName
  4. 在类中声明tagName
  5. 以上都没有的情况下使用默认的'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,把视图对象可能的被外部的侦听也断开。

【转】Backbone使用总结的更多相关文章

  1. Backbone源码解析(六):观察者模式应用

    卤煮在大概一年前写过backbone的源码分析,里面讲的是对一些backbone框架的方法的讲解.这几天重新看了几遍backbone的源码,才发现之前对于它的理解不够深入,只关注了它的一些部分的细节和 ...

  2. MVC、MVP、MVVM、Angular.js、Knockout.js、Backbone.js、React.js、Ember.js、Avalon.js、Vue.js 概念摘录

    注:文章内容都是摘录性文字,自己阅读的一些笔记,方便日后查看. MVC MVC(Model-View-Controller),M 是指业务模型,V 是指用户界面,C 则是控制器,使用 MVC 的目的是 ...

  3. 使用backbone的history管理SPA应用的url

    本文介绍如何使用backbone的history模块实现SPA应用里面的URL管理.SPA应用的核心在于使用无刷新的方式更改url,从而引发页面内容的改变.从实现上来看,url的管理和页面内容的管理是 ...

  4. Backbone中的model和collection在做save或者create操作时, 如何选择用POST还是PUT方法 ?

    Model和Collection和后台的WEB server进行数据同步非常方便, 都只需要在实行里面添加一url就可以了,backbone会在model进行save或者collection进行cre ...

  5. Backbone.js 中的Model被Destroy后,不能触发success的一个原因

    下面这段代码中, 当调用destroy时,backbone会通过model中的url,向服务端发起一个HTTP DELETE请求, 以删除后台数据库中的user数据. 成功后,会回调触发绑定到dest ...

  6. Backbone.js应用基础

    前言: Backbone.js是一款JavaScript MVC应用框架,强制依赖于一个实用型js库underscore.js,非强制依赖于jquery:其主要组件有模型,视图,集合,路由:与后台的交 ...

  7. Backbone事件模块及其用法

    事件模块Backbone.Events在Backbone中占有十分重要的位置,其他模块Model,Collection,View所有事件模块都依赖它.通过继承Events的方法来实现事件的管理,可以说 ...

  8. Backbone源码学习之extend

    extend函数在backbone大概就20来行代码包括注释,对学习Javascript中"类"的继承很是好的学习资料. 下面先贴出Backbone中的源码,其中涉及到unders ...

  9. Backbone,Marionette,Talent学习笔记

    具体以源码为准 Talent继承自Marionette继承自BackBone Region: 继承自Backbone.Event,show(view)会调用view.render(),然后$el.ap ...

  10. backbone入门示例

    最近因为有个项目需要用backbone+mui  所以最近入坑backbone. Backbonejs有几个重要的概念,先介绍一下:Model,Collection,View,Router.其中Mod ...

随机推荐

  1. 在iis中设置文件下载而不是在浏览器上打开

    点击网页链接的*.txt,*.jpg,*.xml等文件时会在浏览器上直接显示,并没有像*.doc那样弹出下载提示框. 解决方法: 在部署的网站上,选择存放文件的目录,选择 HTTP响应标头 ,添加一个 ...

  2. myeclipse 手动安装 lombok

    1. 将 lombok.jar 复制到 myeclipse.ini / eclipse.ini 所在的文件夹目录下    2. 打开 eclipse.ini / myeclipse.ini,在最后面插 ...

  3. thinkphp 验证

    //YongHuModel.class.php namespace Home\Model; use Think\Model; class YongHuModel exstends Model { pr ...

  4. ASPNET 导出EXCEL表

    其实网上有很多关于Excel的例子,但是不是很好,他们的代码没有很全,读的起来还很晦涩.经过这几天的摸索,终于可以完成我想要导出报表Excel的效果了.下面是我的效果图. 一.前台的页面图 GridV ...

  5. Flask+Nginx+uWSGI在Ubuntu服务器上的配置

    Flask+Nginx+uWSGI在Ubuntu服务器上的配置 Step1 安装系统环境 Ubuntu服务器选择是阿里云的ECS服务,ECS提供单独的内存\CPU\带宽\存储规格可以选择,并且提供合适 ...

  6. Oracle数据库迁移到AWS云的方案

    当前云已经成为常态,越来越多的企业希望使用云来增加基础设施的弹性.减轻基础设施的维护压力,运维的成本等.很多企业使用云碰到的难题之一是如何将现有的应用迁移到云上,将现有应用的中间件系统.Web系统及其 ...

  7. windows获取硬盘使用率等信息

    #coding=utf8 import psutil cpu = {'user' : 0, 'system' : 0, 'idle' : 0, 'percent' : 0} mem = {'total ...

  8. 动态加载jar包中的类(方式一)

    嘛, 直接上代码 public static class TestClassLoader extends ClassLoader { @Override protected Class<?> ...

  9. js如何找到方法在哪个js文件

    在Console窗口中输入var f = methodA.prototype.constructor;console.log(f); 网络搜索到的方法.

  10. 【SQLServer】“无法对数据库’XXX’ 执行删除,因为它正用于复制”的解决方法

    警告: 一个或多个现有列的 ANSI_PADDING 设置为 "off",将以 ANSI_PADDING 为 "on" 的设置重新创建 [SQLServer]& ...