摘要:原文使用总结事件模型及其原理就是事件实现的核心,它可以让对象拥有事件能力对象通过侦听其他对象,通过触发事件。可以脱离的,在自定义的对象上使用事件执行结果的和等核心类,都是继承自的。在内部使用事件基类的完成这个动作。
开始在项目中大规模使用backbone,一路磕磕碰碰,边做边学习边体会,有一些心得和体会,记录在本文中。原文: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属性或者是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最后是否包含"/"的不确定性。
Model和Collection的关系这个正则匹配的是行末的非/字符,这样,像/resources这样的目标会匹配s,然后replace中使用分组编号$1捕获了s,将s替换为s/,这样就自动加上了缺失的/;而当/resources/这样目标却无法匹配到结果,也就不需要替换了。
在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的引用关系
自动模型构造利用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(){}语法,会因为对象缺乏属性而报错。
视图的elBackbone的视图对象十分简答,对于开发者而言,仅仅关心一个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("<%= id %> <%= summary %> <%= description %> "), 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,把视图对象可能的被外部的侦听也断开。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/107005.html
摘要:应用的功能这个应用是一个个人简历生成器。比较好的教程有这一个。这样的命名污染问题自然显而易见。而且发出多次请求也会影响性能。明显不利于维护。然而我希望能够不发生变化,因为是在文件的前提下的标签页,不能换一个标签就重建一个。 为什么学习backbone?这是个好问题。在这个前端框架爆炸的年代,比起backbone,对开发来说有更多更好的选择,react,vue,angular等等。但这些...
摘要:原文精髓,观察者模式和事件交互逻辑更需要设计模式设计模式将人们在以往的开发过程中的经验加以总结,以指导后人。的事件根据上面讨论,要实现观察者模式,事件是非常重要的机制。总结虽然是模式的框架,但是其核心却是界面的观察者模式和事件机制。 前言 本人并非专业的前端,只是由于需要被迫转做一段时间的前端,一段时间以来开始探索javascript上的MVC模式,最终打算从Backbone下手。在...
1. 开场 1.1 MVC? MVC是一种GUI软件的一种架构模式。它的目的是将软件的数据层(Model)和视图(view)分开。Model连接数据库,实现数据的交互。用户不能直接和数据打交道,而是需要通过操作视图,然后通过controller对事件作出响应,最后才得以改变数据。最后数据改变,通过观察者模式更新view。(所以在这里需要用到设计模式中的观察者模式) 1.2 Smalltalk-80...
摘要:所以大量的问题都留给开发者自己想办法来解决,因此遭到吐槽当然,使用纯开发一个复杂应用时,情况就会变得非常糟糕。管理复杂的交互自己维护。影响了集合的排列。在中调用这样做是不对的,因为会让应用越来越复杂的。 只扯蛋,不给代码,就是耍流氓 -- honger。 完整的 tutorial 代码 戳这里, 因为我使用的是 commonjs 规范,基于 spm 的,你可以先安装,然后运行它。更多 ...
摘要:原文译者飞龙协议这个例子展示了如何在的引擎中使用模型。在年三月首次作为的一部分发布,并通过以原生方式在上运行脚本扩展了的功能。将二者放在一起下一个目标是在中,例如在服务器上复用模型。最后,我们在中调用函数。总结在中复用现存的库十分简单。 原文:Using Backbone.js with Nashorn 译者:飞龙 协议:CC BY-NC-SA 4.0 这个例子展示了如何在Java8...
阅读 3093·2019-08-30 15:55
阅读 2933·2019-08-30 13:46
阅读 1395·2019-08-29 17:29
阅读 3468·2019-08-29 11:08
阅读 3419·2019-08-29 11:04
阅读 1063·2019-08-28 18:20
阅读 529·2019-08-26 13:37
阅读 1309·2019-08-26 11:49