深入解析JavaScript框架Backbone.js中的事件机制


Posted in Javascript onFebruary 14, 2016

事件模型及其原理
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');

执行结果:

深入解析JavaScript框架Backbone.js中的事件机制

Backbone的Model和View等核心类,都是继承自Backbone.Events的。例如Backbone.Model:

var Events = Backbone.Events = { .. }

var Model = Backbone.Model = function(attributes, options) {
 ...
};

_.extend(Model.prototype, Events, { ... })

从原理上讲,事件是这么工作的:

被侦听的对象维护一个事件数组_event,其他对象在调用listenTo时,会将事件名与回调维护到队列中:

深入解析JavaScript框架Backbone.js中的事件机制

一个事件名可以对应多个回调,对于被侦听者而言,只知道回调的存在,并不知道具体是哪个对象在侦听它。当被侦听者调用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最后是否包含'/'的不确定性。

这个正则匹配的是行末的非/字符,这样,像/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属性可以通过五种途径给出,优先级从高到低:

  • 实例化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):

深入解析JavaScript框架Backbone.js中的事件机制

之所以用child过滤,因为我们的类继承自Backbone的类型,而继承使用了重写原型的方法,Backbone在继承时,使用的变量名为child,最后,child被返回出来了
点击排序后,再次抓取快照,可以看到实例个数变成了14个,这是因为,在render过程中,又创建了4个新的TaskView,而之前的4个TaskView并没有释放(之所以是4个是因为记录的条数是4)

深入解析JavaScript框架Backbone.js中的事件机制

再次点击排序,再次抓取快照,实例数又增加了4个,变成了18个!

深入解析JavaScript框架Backbone.js中的事件机制

那么,为什么每次排序后,之前的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>

事件委托 格式:事件 + 空格 + 由谁来触发 : 对应的回调函数

Javascript 相关文章推荐
HTML页面登录时的JS验证方法
May 28 Javascript
js调试系列 初识控制台
Jun 18 Javascript
基于javascript实现图片懒加载
Jan 05 Javascript
jquery结合html实现中英文页面切换
Nov 29 Javascript
基于javascript实现按圆形排列DIV元素(三)
Dec 02 Javascript
Bootstrap实现各种进度条样式详解
Apr 13 Javascript
vue-cli3 取消eslint校验代码的解决办法
Jan 16 Javascript
Vue中import from的来源及省略后缀与加载文件夹问题
Feb 09 Javascript
javascript中innerHTML 获取或替换html内容的实现代码
Mar 17 Javascript
JS浏览器BOM常见操作实例详解
Apr 27 Javascript
JQuery基于FormData异步提交数据文件
Sep 01 jQuery
javascript的setTimeout()使用方法总结
Nov 20 Javascript
Node.js 条形码识别程序构建思路详解
Feb 14 #Javascript
jQuery插件支持同一页面被多次调用
Feb 14 #Javascript
JavaScript中通过提示框跳转页面的方法
Feb 14 #Javascript
JavaScript中关联原型链属性特性
Feb 13 #Javascript
JavaScript操作class和style样式代码详解
Feb 13 #Javascript
javascript实现查找数组中最大值方法汇总
Feb 13 #Javascript
JavaScript常用数组算法小结
Feb 13 #Javascript
You might like
PHP面向对象自动加载机制原理与用法分析
2016/10/14 PHP
PHP文件与目录操作示例
2016/12/24 PHP
JavaScript Sort 的一个错误用法示例
2015/03/20 Javascript
谈谈JavaScript异步函数发展历程
2015/09/29 Javascript
浅谈javascript中replace()方法
2015/11/10 Javascript
javascript电商网站抢购倒计时效果实现
2015/11/19 Javascript
详解JS正则replace的使用方法
2016/03/06 Javascript
AngularJS基础 ng-src 指令简单示例
2016/08/03 Javascript
EasyUI修改DateBox和DateTimeBox的默认日期格式示例
2017/01/18 Javascript
JS实现的模仿QQ头像资料卡显示与隐藏效果
2017/04/07 Javascript
浅谈Angular2 模块懒加载的方法
2017/10/04 Javascript
vue router demo详解
2017/10/13 Javascript
echarts整合多个类似option的方法实例
2018/07/10 Javascript
后台使用freeMarker和前端使用vue的方法及遇到的问题
2019/06/13 Javascript
layui多iframe页面控制定时器运行的方法
2019/09/05 Javascript
vue-cli4.0多环境配置变量与模式详解
2020/12/30 Vue.js
详解vite2.0配置学习(typescript版本)
2021/02/25 Javascript
[15:09]DOTA2国际邀请赛采访专栏:Loda
2013/08/06 DOTA
python从ftp下载数据保存实例
2013/11/20 Python
Python Sleep休眠函数使用简单实例
2015/02/02 Python
Python中for循环和while循环的基本使用方法
2015/08/21 Python
Python利用正则表达式实现计算器算法思路解析
2018/04/25 Python
opencv python 图像去噪的实现方法
2018/08/31 Python
Python解析、提取url关键字的实例详解
2018/12/17 Python
python判断字符串或者集合是否为空的实例
2019/01/23 Python
django 前端页面如何实现显示前N条数据
2020/03/16 Python
python 5个实用的技巧
2020/09/27 Python
使用CSS3配合IE滤镜实现渐变和投影的效果
2015/09/06 HTML / CSS
HTML5 播放 RTSP 视频的实例代码
2019/07/29 HTML / CSS
苏格兰领先的多渠道鞋店:Begg Shoes
2019/10/22 全球购物
澳大利亚宠物食品和用品商店:PETstock
2020/01/02 全球购物
吨的认识教学反思
2014/04/27 职场文书
竞选生活委员演讲稿
2014/04/28 职场文书
基层党建工作简报
2015/07/21 职场文书
学校趣味运动会开幕词
2016/03/04 职场文书
开机音效回归! Windows 11重新引入开机铃声
2021/11/21 数码科技