现在的位置: 首页 > Frameworks > Backbone > 正文
Backbone事件模型概述
2017年04月09日 Backbone ⁄ 共 8576字 暂无评论 ⁄ 被围观 336 views+
文章目录
[隐藏]

Backbone主要包含Events, Model, Collection, Route, History, Sync, View 这几个模块, 其中又以Events, Model, View为核心,本文抛开其它模块,单独谈谈Backbone的Events模块。

为什么要有事件模块

原因很简单:我们需要一种能监听普通对象属性变化的手段。

为什么强调是普通对象

基本所有浏览器都提供了DOM对象的监听,比如按钮的onclick, 输入框的onchange事件等。所以对于DOM对象,我们是有方法监听其变化的。

但是针对普通JavaScript对象属性的监控方案,在早期的规范里面没有定义,一直到 ECMAScript 5 版本,定义对象时,才可以通过定义对象属性的set方法来监听对象的属性变化。

为什么需要监听普通对象

简单的一句话就是随着富客户端应用的普及,前端代码变得庞大臃肿,难以维护,于是一些好的设计模式被搬到前端应用中,MVVM, MVC等等,对普通对象的监听能力,有时候就是在这些设计模式的引入中被提出的。

在Backbone中,对普通对象属性的监听能力是Model和View两大核心模块的纽带,Model就是普通对象,如果想根据Model的变化去更新视图(View),对Model对象的监听能力是必须的。

如何实现对普通对象的监听

Backbone的实现其实并没有多少奥秘,事实上,整个Events模块的代码量也就两三百行,而实现的方案就是PUB/SUB模式

如何使用Backbone的Events模块

只要让一个普通对象继承Backbone.Events就可以了。至于如何继承,常用的工具类中有 jQuery.extend, _.extend两个方法可以实现。而这里继承的本质,实际上就是将Backbone.Events对象定义的几个API方法附加到普通对象上。

这里顺便提一下,Backbone强依赖underscore,不过你可以换成lodash这个骚包,性能设计得比underscore出色不少,而且API完全兼容。本文的所有示例代码都使用 _.extend (underscore或者lodash)来继承Backbone.Events。

小试牛刀

先定义一个普通的JavaScript对象,该对象有一个属性 lover

  1. // 定义一个对象
  2. var man = {
  3.     lover: "钟丽缇" // 这个男人的爱人是钟丽缇
  4. };

接下来,让 man 对象继承 Backbone.Events

  1. // 将Backbone.Events的API附加到man对象上
  2. _.extend(man, Backbone.Events);

我们来看下继承 Backbone.Events 之后,man 对象多了哪些属性,在浏览器控制台把 man 对象打印出来:

继承之后的对象属性

可以看到除了 man 对象原本的lover属性外,多了一系列函数,这些函数便是从 Backbone.Events 继承来的,此时 on, off 等API就可以正常使用了。

进一步,随便定义个事件来监控 man 对象, 事件命名为 change

  1. // 为 man 对象定义一个事件,事件名完全自定义,此处我们将事件命名为 change
  2. man.on('change', function() {
  3.     // 此处是事件处理函数
  4.     console.log("change事件被触发");
  5. });

事件定义好了,如何触发它呢,是不是因为事件名是 change 事件,改变 manlover 属性就能触发该事件呢? 事实很快证明,比如设置 man.lover = "赵雅芝" 不会触发任何事件。

Backbone没有什么黑魔法,想要触发事件,需要自己去派发事件,用 trigger 接口去派发事件:

  1. // 派发事件
  2. man.trigger("change");

运行一下该脚本,可以看到控制台输出: change事件被触发

牛刀试过了,好像也没那么牛么。这也证明了 Backbone.Events 确实没有什么黑魔法。使用方式两步:

  • 使用on,once,listenTo,listenToOnce接口将事件绑定到对象上;
  • 使用trigger接口触发绑定的事件处理程序;

如果需要解绑事件,则使用off(针对on,once)或者stopListening(针对listenTo,listenToOnce)进行事件的删除。

Backbone.Events API详解

on接口

功能:填加自定义事件到对象
语法:object.on(event, [callback], [context])

参数说明
  • event,需要绑定的事件,必选参数;
  • callback,事件处理函数,可选参数,在event参数为对象的时候,该参数可以省略;
  • context,执行上下文,也就是事件处理函数中this的引用(在事件处理函数中this===context),如果不指定贷参数,事件处理函数的上下文为接口的调用者,即object
事件的绑定风格

on的三个入参中,event的格式比较灵活,支持三种类型的事件格式:

标准风格
这是最常用的方式,此时event参数为不含空白符的字符串,也就是单一事件,例如object.on("change", callback)

jQuery风格
此时为多个事件,每个事件以空白符做分割,例如object.on("change add remove", callback)绑定了change,add,remove三个事件,而这三个事件的事件处理函数都是callback特别注意的是Backbone不支持逗号分割

Map风格
Map风格的事件是个对象,对象的每个属性是事件名,属性值是事件处理函数,例如:

  1. man.on({
  2.     'change': function(args) {
  3.         console.log('change');
  4.     },
  5.     'add': function(args) {
  6.         console.log('add');
  7.     },
  8.     'remove': function(args) {
  9.         console.log('remove');
  10.     }
  11. });

以上代码一次性绑定了三个事件,并且分别为其定义了事件处理函数。

map风格的事件绑定有着比较特殊的地方:

如果on接口只传了两个参数,则即第二个参数callback的语意发生变化,因为事件处理函数已经在event中指定,所以callback参数的语意提升为context,例如:

  1. // 定义另一个对象woman
  2. var woman = {
  3.     description: "I am a woman"
  4. }
  5. // 绑定 add 和 remove事件,并且将上下文设置为woman
  6. man.on({
  7.     'add': function(args) {
  8.         // 控制台输出:I am a woman
  9.         // 说明上下文是woman对象
  10.         console.log(this.description);
  11.     },
  12.     'remove': function(args) {
  13.         // 因为只派发了add事件,不会执行remove事件的处理函数
  14.         console.log(this.description);
  15.     }
  16. },woman);
  17. // 只派发add事件
  18. man.trigger("add");

如果on接口只传了三个参数,则callback参数会被忽略,context依旧为context,例如:

  1. // 定义另一个对象woman
  2. var woman = {
  3.     description: "I am a woman"
  4. }
  5. // 定义一个对象 cat
  6. var cat = {
  7.     description: "I am Hello kitty"
  8. }
  9. // 绑定 add 和 remove事件,并且将上下文设置为cat
  10. man.on({
  11.     'add': function(args) {
  12.         // 控制台输出:I am Hello kitty
  13.         // 说明上下文是cat对象
  14.         console.log(this.description);
  15.     },
  16.     'remove': function(args) {
  17.         // 没有派发remove事件,不会执行该函数
  18.         console.log(this.description);
  19.     }
  20. }, woman, cat);
  21. // 派发add事件
  22. man.trigger("add");

那么,如何为每个事件处理函数指定上下文呢?可以使用 JavaScript 的原生函数bind来解决(如果要兼容低版本IE,则需要使用underscore提供的的bind函数):

  1. // 绑定 add 和 remove事件,并且分别设置上下文
  2. man.on({
  3.     'add': function(args) {
  4.         // 输出added: I am a woman
  5.         console.log('added: ' + this.description);
  6.     }.bind(woman),
  7.     'remove': function(args) {
  8.         // 输出removed: I am Hello kitty
  9.         console.log('removed: ' + this.description);
  10.     }.bind(cat)
  11. });
  12. // 派发事件
  13. man.trigger("add remove");
一个事件绑定多个callback

和jQuery对DOM事件的绑定一样,同一个事件可以绑定多个事件处理函数,当事件被触发时,多个函数按照绑定的顺序依次调用,例如:

  1. var man = {
  2.     lover: "钟丽缇"
  3. };
  4. _.extend(man, Backbone.Events);
  5. // 绑定change事件
  6. man.on('change', function() {
  7.     console.log("I am the first callback");
  8. });
  9. // 再次绑定change事件
  10. man.on('change', function() {
  11.     console.log("I am the second callback");
  12. });
  13. man.trigger("change");

运行代码后,可以看到两个事件函数,按照绑定的顺序依次被调用,向控制台打印信息。

多个事件共用一个callback
  1. var man = {
  2.     lover: "钟丽缇"
  3. };
  4. _.extend(man, Backbone.Events);
  5. function showMsg() {
  6.     console.log(this.lover);
  7. }
  8. man.on({
  9.     'add': showMsg,
  10.     'remove': showMsg
  11. });
  12. // 派发事件
  13. man.trigger("add remove");
bind接口

功能:填加自定义事件到对象
语法:object.bind(event, [callback], [context])

bindon接口的一个别名,用法完全一样,之所以有这个别名,主要也是为了兼容传统的bind写法。

once接口

功能:填加只执行一次的自定义事件到对象
语法:object.once(event, [callback], [context])

onceon接口的用法基本一样,只不过事件对应的处理函数只会执行一次,即,多次派发该事件,只会触发一次callback,例如:

  1. var man = {
  2.     lover: "钟丽缇"
  3. };
  4. _.extend(man, Backbone.Events);
  5. man.once("change",
  6. function() {
  7.     console.log("I am changed");
  8. });
  9. // 两次派发change事件
  10. // 只有第一次派发change事件的时候才会调用callback
  11. man.trigger("change");
  12. man.trigger("change");
消失的上下文

on接口的三个参数里,如果不指定context参数的话,默认的上下文就是object,但是once接口却把这个默认的上下文给弄丢了,例如:

  1. var man = {
  2.     lover: "钟丽缇"
  3. };
  4. _.extend(man, Backbone.Events);
  5. man.once("change",
  6. function() {
  7.     // 控制台只打印: My lover is undefined
  8.     // 可见once接口并没有将man作为默认的上下文
  9.     console.log("My lover is " + this.lover);
  10. });
  11. man.trigger("change");

个人认为这个结果是不合理的,至少破坏了接口用法的一致性,至于Backbone有什么其他的考虑,暂时不得而知。

listenTo接口

功能:监听另外一个对象(object是监听者;other是被监听者)
语法:object.listenTo(other, event, callback)

不同对象之间的监听

有些场景事件的处理函数并不一定当前对象。例如当Model变化时,我们需要触发View的一些行为,让View自动更新;又比如摄像头负责监控场地,发现险情时,需要触发的是警报器的报警动作,摄像头本身是没有报警效果的。

比如view监听model的简单例子:

  1. view.listenTo(model, 'change', function() {
  2.     // 这个时间处理函数的上下文是view对象,即this===view
  3.     // 更新页面的DOM(此处是伪代码)
  4.     this.updateDom();
  5. });

其实,on接口同样可以实现,只要指定一下context参数就可以了。不过语义上讲,用listenTo接口更合理,例如下面的代码也可以实现上述功能,但是可读性要差一点:

  1. model.on('change', function() {
  2.     // 这个时间处理函数的上下文是view对象,即this===view
  3.     // 更新页面的DOM(此处是伪代码)
  4.     this.updateDom();
  5. }, view);
方便事件的统一维护

listenTo接口不仅仅是为了提高代码的可读性,它同时还提高了时间的可维护性。例如A对象监听了B,C,D三个对象,当我们需要一次性移除所有事件的时候,用stopListening接口很方便:

  1. // A对象监听了三个对象B,C,D
  2. A.listenTo(B, 'change', function() {});
  3. A.listenTo(C, 'change', function() {});
  4. A.listenTo(D, 'change', function() {});
  5. // 一次性移除所有事件
  6. A.stopListening();

如果使用on接口实现的话,则写起来比较繁琐,移除事件的时候需要一个个移除:

  1. // A对象监听了三个对象B,C,D
  2. B.on('change', function() {}, A);
  3. C.on('change', function() {}, A);
  4. D.on('change', function() {}, A);
  5. // B,C,D分别移除事件,代码比较listenTo方式显然要臃肿很多 
  6. B.off('change');
  7. C.off('change');
  8. D.off('change');
listenToOnce接口

功能:listenTo接口的once版本,监听事件只会执行一次
语法:object.listenToOnce(other, event, callback)

once接口会丢失默认的上下文,而listenToOnce接口并不存在这个问题,因为listenToOnce接口天生就是为不同对象之间的监听设计的,不可能把监听者给弄丢了。

off接口

功能:移除绑定到对象的事件
语法:object.off([event], [callback], [context])

事件的存储结构

在说off接口之前,先要提一下,Backbone内部是如何储存绑定到对象上的事件的。所有的事件都存储在对象的_events属性上。_events大致的数据结构是:先哈希后队列再哈希,比如我们在 A 对象上绑定以下事件:

  1. var A = _.extend({
  2.     name: 'A',
  3.     say: function() {
  4.         console.log(this.name);
  5.     }
  6. },
  7. Backbone.Events);
  8. var context_1 = {
  9.     name: 'Context one'
  10. };
  11. var context_2 = {
  12.     name: 'Context two'
  13. };
  14. var callback_1 = function() {
  15.     console.log('callback_1: context is ' + this.name)
  16. };
  17. var callback_2 = function() {
  18.     console.log('callback_2: context is ' + this.name)
  19. };
  20. A.on('change', callback_1, context_1);
  21. A.on('change', callback_1, context_1);
  22. A.on('add', callback_2, context_1);
  23. A.trigger('change add');

在控制台把A对象打印出来,看A._events的数据结构:

add和change两个事件分别注册了一个key,而change事件因为有两个事件处理函数(绑定了两次),_.events.change值是一个长度为2的数组。

我们还可以看到,每个事件处理函数都被包装成了一个事件处理对象,该对象包含4个属性callback(对事件处理函数的引用),ctx(实际的上下文),context(Backbone内部使用),listening(监听对象,采用listenTo方式绑定的时候保存监听对象的信息)

参数说明

弄清楚事件存储结构后,off接口的三个参数就更容易理解了,其实就是三个可选的组合过滤条件,条件匹配则移除事件,当传多个参数的时候,必须同时满足条件的事件才会被移除。

  • event:事件名,可选。检索_.events的key,相等的话(字符串比较)就匹配上;
  • callback:回调函数,可选。检索所有的事件处理对象的callback属性,相等的话(对象比较)就匹配上;
  • context:执行上下文,可选。检索所有的事件处理对象的ctx属性,相等的话(对象比较)就匹配上;

以下列举了一些例子:

  1. var A = _.extend({
  2.     name: 'A',
  3.     say: function() {
  4.         console.log(this.name);
  5.     }
  6. },
  7. Backbone.Events);
  8. var context_1 = {
  9.     name: 'Context one'
  10. };
  11. var context_2 = {
  12.     name: 'Context two'
  13. };
  14. var callback_1 = function() {
  15.     console.log('callback_1: context is ' + this.name)
  16. };
  17. var callback_2 = function() {
  18.     console.log('callback_2: context is ' + this.name)
  19. };
  20. A.on('change', callback_1, context_1);
  21. A.on('change', callback_1, context_2);
  22. A.on('change', callback_2, context_1);
  23. A.on('change', callback_2, context_2);
  24. A.on('add', callback_1, context_1);
  25. A.on('add', callback_1, context_2);
  26. A.on('add', callback_2, context_1);
  27. A.on('add', callback_2, context_2);
  28. // 移除所有上下文为context_1的事件
  29. A.off(falsefalse, context_1);
  30. // 移除change事件
  31. A.off('change');
  32. // 移除 事件名为 change 且 事件处理函数为 callback_1 且 上下文为context_2的事件
  33. A.off('change', callback_1, context_2);
  34. A.trigger('change add');

给我留言

留言无头像?


×