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
- // 定义一个对象
- var man = {
- lover: "钟丽缇" // 这个男人的爱人是钟丽缇
- };
接下来,让 man 对象继承 Backbone.Events
- // 将Backbone.Events的API附加到man对象上
- _.extend(man, Backbone.Events);
我们来看下继承 Backbone.Events
之后,man
对象多了哪些属性,在浏览器控制台把 man
对象打印出来:
可以看到除了 man
对象原本的lover属性外,多了一系列函数,这些函数便是从 Backbone.Events
继承来的,此时 on
, off
等API就可以正常使用了。
进一步,随便定义个事件来监控 man
对象, 事件命名为 change
- // 为 man 对象定义一个事件,事件名完全自定义,此处我们将事件命名为 change
- man.on('change', function() {
- // 此处是事件处理函数
- console.log("change事件被触发");
- });
事件定义好了,如何触发它呢,是不是因为事件名是 change
事件,改变 man
的 lover
属性就能触发该事件呢? 事实很快证明,比如设置 man.lover = "赵雅芝"
不会触发任何事件。
Backbone没有什么黑魔法,想要触发事件,需要自己去派发事件,用 trigger
接口去派发事件:
- // 派发事件
- 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风格的事件是个对象,对象的每个属性是事件名,属性值是事件处理函数,例如:
- man.on({
- 'change': function(args) {
- console.log('change');
- },
- 'add': function(args) {
- console.log('add');
- },
- 'remove': function(args) {
- console.log('remove');
- }
- });
以上代码一次性绑定了三个事件,并且分别为其定义了事件处理函数。
map风格的事件绑定有着比较特殊的地方:
如果on
接口只传了两个参数,则即第二个参数callback
的语意发生变化,因为事件处理函数已经在event
中指定,所以callback
参数的语意提升为context
,例如:
- // 定义另一个对象woman
- var woman = {
- description: "I am a woman"
- }
- // 绑定 add 和 remove事件,并且将上下文设置为woman
- man.on({
- 'add': function(args) {
- // 控制台输出:I am a woman
- // 说明上下文是woman对象
- console.log(this.description);
- },
- 'remove': function(args) {
- // 因为只派发了add事件,不会执行remove事件的处理函数
- console.log(this.description);
- }
- },woman);
- // 只派发add事件
- man.trigger("add");
如果on
接口只传了三个参数,则callback
参数会被忽略,context
依旧为context
,例如:
- // 定义另一个对象woman
- var woman = {
- description: "I am a woman"
- }
- // 定义一个对象 cat
- var cat = {
- description: "I am Hello kitty"
- }
- // 绑定 add 和 remove事件,并且将上下文设置为cat
- man.on({
- 'add': function(args) {
- // 控制台输出:I am Hello kitty
- // 说明上下文是cat对象
- console.log(this.description);
- },
- 'remove': function(args) {
- // 没有派发remove事件,不会执行该函数
- console.log(this.description);
- }
- }, woman, cat);
- // 派发add事件
- man.trigger("add");
那么,如何为每个事件处理函数指定上下文呢?可以使用 JavaScript 的原生函数bind
来解决(如果要兼容低版本IE,则需要使用underscore
提供的的bind
函数):
- // 绑定 add 和 remove事件,并且分别设置上下文
- man.on({
- 'add': function(args) {
- // 输出added: I am a woman
- console.log('added: ' + this.description);
- }.bind(woman),
- 'remove': function(args) {
- // 输出removed: I am Hello kitty
- console.log('removed: ' + this.description);
- }.bind(cat)
- });
- // 派发事件
- man.trigger("add remove");
一个事件绑定多个callback
和jQuery对DOM事件的绑定一样,同一个事件可以绑定多个事件处理函数,当事件被触发时,多个函数按照绑定的顺序依次调用,例如:
- var man = {
- lover: "钟丽缇"
- };
- _.extend(man, Backbone.Events);
- // 绑定change事件
- man.on('change', function() {
- console.log("I am the first callback");
- });
- // 再次绑定change事件
- man.on('change', function() {
- console.log("I am the second callback");
- });
- man.trigger("change");
运行代码后,可以看到两个事件函数,按照绑定的顺序依次被调用,向控制台打印信息。
多个事件共用一个callback
- var man = {
- lover: "钟丽缇"
- };
- _.extend(man, Backbone.Events);
- function showMsg() {
- console.log(this.lover);
- }
- man.on({
- 'add': showMsg,
- 'remove': showMsg
- });
- // 派发事件
- man.trigger("add remove");
bind
接口
功能:填加自定义事件到对象
语法:object.bind(event, [callback], [context])
bind
是on
接口的一个别名,用法完全一样,之所以有这个别名,主要也是为了兼容传统的bind
写法。
once
接口
功能:填加只执行一次的自定义事件到对象
语法:object.once(event, [callback], [context])
once
和on
接口的用法基本一样,只不过事件对应的处理函数只会执行一次,即,多次派发该事件,只会触发一次callback
,例如:
- var man = {
- lover: "钟丽缇"
- };
- _.extend(man, Backbone.Events);
- man.once("change",
- function() {
- console.log("I am changed");
- });
- // 两次派发change事件
- // 只有第一次派发change事件的时候才会调用callback
- man.trigger("change");
- man.trigger("change");
消失的上下文
在on
接口的三个参数里,如果不指定context
参数的话,默认的上下文就是object
,但是once
接口却把这个默认的上下文给弄丢了,例如:
- var man = {
- lover: "钟丽缇"
- };
- _.extend(man, Backbone.Events);
- man.once("change",
- function() {
- // 控制台只打印: My lover is undefined
- // 可见once接口并没有将man作为默认的上下文
- console.log("My lover is " + this.lover);
- });
- man.trigger("change");
个人认为这个结果是不合理的,至少破坏了接口用法的一致性,至于Backbone有什么其他的考虑,暂时不得而知。
listenTo
接口
功能:监听另外一个对象(object
是监听者;other
是被监听者)
语法:object.listenTo(other, event, callback)
不同对象之间的监听
有些场景事件的处理函数并不一定当前对象。例如当Model
变化时,我们需要触发View
的一些行为,让View
自动更新;又比如摄像头负责监控场地,发现险情时,需要触发的是警报器的报警动作,摄像头本身是没有报警效果的。
比如view
监听model
的简单例子:
- view.listenTo(model, 'change', function() {
- // 这个时间处理函数的上下文是view对象,即this===view
- // 更新页面的DOM(此处是伪代码)
- this.updateDom();
- });
其实,on
接口同样可以实现,只要指定一下context
参数就可以了。不过语义上讲,用listenTo
接口更合理,例如下面的代码也可以实现上述功能,但是可读性要差一点:
- model.on('change', function() {
- // 这个时间处理函数的上下文是view对象,即this===view
- // 更新页面的DOM(此处是伪代码)
- this.updateDom();
- }, view);
方便事件的统一维护
listenTo
接口不仅仅是为了提高代码的可读性,它同时还提高了时间的可维护性。例如A对象监听了B,C,D三个对象,当我们需要一次性移除所有事件的时候,用stopListening
接口很方便:
- // A对象监听了三个对象B,C,D
- A.listenTo(B, 'change', function() {});
- A.listenTo(C, 'change', function() {});
- A.listenTo(D, 'change', function() {});
- // 一次性移除所有事件
- A.stopListening();
如果使用on接口实现的话,则写起来比较繁琐,移除事件的时候需要一个个移除:
- // A对象监听了三个对象B,C,D
- B.on('change', function() {}, A);
- C.on('change', function() {}, A);
- D.on('change', function() {}, A);
- // B,C,D分别移除事件,代码比较listenTo方式显然要臃肿很多
- B.off('change');
- C.off('change');
- 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 对象上绑定以下事件:
- var A = _.extend({
- name: 'A',
- say: function() {
- console.log(this.name);
- }
- },
- Backbone.Events);
- var context_1 = {
- name: 'Context one'
- };
- var context_2 = {
- name: 'Context two'
- };
- var callback_1 = function() {
- console.log('callback_1: context is ' + this.name)
- };
- var callback_2 = function() {
- console.log('callback_2: context is ' + this.name)
- };
- A.on('change', callback_1, context_1);
- A.on('change', callback_1, context_1);
- A.on('add', callback_2, context_1);
- 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属性,相等的话(对象比较)就匹配上;
以下列举了一些例子:
- var A = _.extend({
- name: 'A',
- say: function() {
- console.log(this.name);
- }
- },
- Backbone.Events);
- var context_1 = {
- name: 'Context one'
- };
- var context_2 = {
- name: 'Context two'
- };
- var callback_1 = function() {
- console.log('callback_1: context is ' + this.name)
- };
- var callback_2 = function() {
- console.log('callback_2: context is ' + this.name)
- };
- A.on('change', callback_1, context_1);
- A.on('change', callback_1, context_2);
- A.on('change', callback_2, context_1);
- A.on('change', callback_2, context_2);
- A.on('add', callback_1, context_1);
- A.on('add', callback_1, context_2);
- A.on('add', callback_2, context_1);
- A.on('add', callback_2, context_2);
- // 移除所有上下文为context_1的事件
- A.off(false, false, context_1);
- // 移除change事件
- A.off('change');
- // 移除 事件名为 change 且 事件处理函数为 callback_1 且 上下文为context_2的事件
- A.off('change', callback_1, context_2);
- A.trigger('change add');