Events就作为分析Backbone1.0的头一篇吧,如果大家觉得还行,在继续分析,否则就只能作罢了,免得一片骂声。
在开篇以前先讨论个小问题做个引子:Events能否单独使用。经常有人说不可以,其实不是不可以,而是没人会这么用。看下Events的定义:
var Events = Backbone.Events = { on:... , off:.... . . . }可以看到,Event本身就是一个对象,完全可以 Backbone.Events.on("change", myFunc, myContent);这样用。我们知道在Backbone中事件是基于对象存在的,事件的所触发的回调函数和上下文都要存储于这个对象中的某个属性中,每个model实例或view实例都会有这样一个单独的属性。而Events是在Backbone范围内的一个全局变量,是公共的,把所有事件都放在公共的Events,管理起来太难了。所以Events存在的目的是被其他组件(Model,View, collection等)继承的,而不是单独使用的。
而Backbone.Model和Backbone.View之所以能够继承Events,是因为有下面俩句:
_.extend(Model.prototype, Events, {...}) 、_.extend(View.prototype, Events, {...}) 该句话的功能是将Events的所有属性复制一份到Model.prototype和View.prototype中,这样每个Model和View的实例,就可以继承Events的所有方法了,而Events所有方法中的this就是指向这些实例了。
注:其中下划线"_"是Backbone依赖库underscore.js的引用,_.extend实际调用的是underscore.js的方法,大家可以参考下它的文档。
注:虽然在js中万物皆对象,但是为了表达方便,还是将函数和对象区分下。而实例仅指通过构造函数生成的对象。
Events所有属性(方法)源码分析
事件名链接字符
// 事件名的连接字符 //为空白字符,如空格、制表符、换页符等等 var eventSplitter = /\s+/;当绑定的事件名为多个时,可用链接字符将这多个事件名链接起来。如:"change blur"
绑定方法(on)
该方法有三个参数:name是事件名称,callback是事件触发时调用的回调函数,context是上下文。
之所以要指定上下文是因为Backbone最终调用回调函数callback时,是用callback.call()或callback.apply()来调用的。
另外,事件名name可以是包含单个事件的字符串,也可以是用链接字符连接的多个事件名,也可是个对象。
1。当事件名是连接符链接的字符串时,需要先将这个字符串分割为多个单独的事件名,在逐个调用on方法。
2。当事件名是个对象时,如{change: action},此时会将对象的每个属性名作为事件名,属性值作为回调方法,此时在调用on方法绑定事件时,不需要在指定回调函数,即:obj.on({change: action}, context)
注:同一个事件名下,可绑定任意个回调函数,当事件被触发时,回调函数会依次被调用
on: function(name, callback, context) { //当事件名称是多个时(即:空白连接符链接的多个事件名或是单个对象),eventsApi方法会将其拦截并处理。 //或者当回调函数callback未指定时,直接返回 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; //_events是存放当前对象所有事件的数据结构,是个对象。它的每个属性都对应一个事件名, //属性值是个数组,存放当前事件下的所有回调函数及上下文。 this._events || (this._events = {});//如果_events未初始化,则进行初始化。 //找到指定事件名下的事件列表。没有事件列表则初始化个空数组。 var events = this._events[name] || (this._events[name] = []); //创建个空对象,将回调函数和上下文存入。然后压入到事件列表中。 //如果上下文context未指定,则用当前对象this作为上下文。 events.push({callback: callback, context: context, ctx: context || this}); return this; }
eventsApi方法是个公用方法,不属于Events对象的属性。该方法主要处理一次绑定多个事件时,即事件名是个对象、或用分隔符链接的字符串时。此方法会在本篇的尾部说明
eventsApi(this, 'on', name, [callback, context]) 参数this是当前对象,参数'on'就是上面的绑定方法,参数name是事件列表。从这里的参数 我们也能大概判断出,最终还是通过调用 this的on方法进行绑定事件的。
下面分析下事件的存储结构:
假设当前对象中已经绑定了"change","blur","all"三个事件名,则它的存储结构应为:
_events{
"change":[{callback: callback, context: context, ctx: context || this},{callback: callback, context: context, ctx: context || this}....],
"blur":[{callback: callback, context: context, ctx: context || this},{callback: callback, context: context, ctx: context || this}....],
"all":[{callback: callback, context: context, ctx: context || this},{callback: callback, context: context, ctx: context || this}....],
}
取消绑定方法(off)
off: function(name, callback, context) { var retain, ev, events, names, i, l, j, k; //当前对象存在事件,并且移除的事件名是数组或分隔符链接的字符串时,调用eventsApi处理,之后直接返回 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; //off未指定参数、或所有参数都为null或undefined时,清空当前对象下的所有事件。 if (!name && !callback && !context) { this._events = {};//清空所有事件 return this; } //将事件名封装到数组中 //如果事件名为空,则调用underscore.js的keys方法取所有事件名,并以数组形式返回。 //此时names中存放的应是即将被移除的事件名 names = name ? [name] : _.keys(this._events); //遍历事件名 for (i = 0, l = names.length; i < l; i++) { name = names[i]; //取出当前事件名下的所有事件存入events中, //如果events存在则进入if if (events = this._events[name]) { //直接清空该事件名下的所有事件 //不用担心,因为上句已经事先将当前事件名下的所有事件存入events中了。 //此时this._events[name]与retain引用同一个数组,所以对retain的操作实际就是对this._events[name] //retain中会存放该事件名下不应该被移除的事件。 this._events[name] = retain = []; if (callback || context) { //遍历事件 for (j = 0, k = events.length; j < k; j++) { ev = events[j]; //判断ev中的事件是否符合被移除的条件。 //如果不符合则通过retain.push(ev)还原回去 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || (context && context !== ev.context)) { retain.push(ev); } } } //如果retain中已经没有事件时,事件名从this._events中移除 if (!retain.length) delete this._events[name]; } } return this; }
一次性绑定方法(once)
once方法是1.0版新添加的方法。也是用于绑定事件的,但它与on的区别是,只能被触发一次,第2次及以后在触发时,回调函数就不执行了。所以我叫它一次性绑定(这名字是我自己起的,可能不大准确)。
once: function(name, callback, context) { //判断是否需要由eventsApi处理 if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; //将当前对象保存到局部变量self中,以便在闭包中使用。 var self = this; //调用underscore.js的once方法,创建一个只会被执行一次的函数, 如果该函数被重复调用, 将返回第一次执行的结果 var once = _.once(function() { //取消绑定,这也就是为啥该事件只能被触发一次了。 self.off(name, once); //调用回调函数callback callback.apply(this, arguments); }); once._callback = callback; //调用on绑定事件,此时的回调函数once是封装后的 return this.on(name, once, context); }
核心就是将回调函数callback进行了封装,在封装后的回调函数中加了取消绑定(即self.off(name, once)句),在事件第1次触发时,执行回调函数的同时也取消了该事件的绑定。
上面的代码中有个双保险,是多余还是另有其他的想法,不得而知:
underscore.js的once方法会将传入的参数(是个函数)进行封装,封装后的函数有俩个特点:
1.被封装的函数只会执行一次
2.多次执行时会返回第一次执行的结果。
这样即使不调用self.off(name, once),回调函数也只是执行一次。况且触发事件时是不需要获得回调函数的返回值的,所以个人觉得用underscore.js的once方法进行二次封装是有点多余的。
触发事件(trigger)
根据指定的事件名name,调用该事件名下绑定的所有回调函数。
trigger: function(name) { //当前对象未绑定事件,直接返回 if (!this._events) return this; //获得参数数组arguments从下标1开始的副本,即去掉事件名的部分 var args = slice.call(arguments, 1); //当参数name为多个事件时,交由eventsApi去处理 if (!eventsApi(this, 'trigger', name, args)) return this; //如果为单事件名则由下面处理 //取出事件名下的所有事件。 var events = this._events[name]; //同时也取出"all"事件下的所有事件。 //这也就是为啥所有事件触发时都会触发"all"事件 var allEvents = this._events.all; //调用triggerEvents方法执行回调函数 if (events) triggerEvents(events, args); if (allEvents) triggerEvents(allEvents, arguments); return this; }
triggerEvents函数
一般由trigger方法调用,通过调用回调函数的call或apply方法直接调用回调函数。
参数:
events:是个数组,包含某个事件名下需要被执行的所有回调函数,每个元素都是形如{callback: callback, context: context, ctx: context || this}的对象
args:调用trigger方法时指定的参数,此参数会作为回调函数的参数
var triggerEvents = function(events, args) { var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; //遍历事件并执行回调函数, //如果回调函数的参数不超过3个,使用callback.call执行。 //如果回调函数的参数超过3个,使用callback.apply执行 switch (args.length) { case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); } };监听(listenTo或listenToOnce)
1.0版本终于提供监听了。这样一个典型的问题就可以很容易实现了:view想要监听model的change事件,就可以这样 view.listenTo(model, 'change', view.render); 其实在以前的版本中实现监听也是很容易的,model.on('change', view.render, view),即"我"要监听"你"的事件,就把"我"的回调函数绑定(on)到"你"的事件里吧。也是异常的简单,其实Backbone的监听也是这样实现的。但是个人觉得专门提供监听组件还是有俩个好处的:
1.增加了代码的可读行,即使不加注释我也能一眼就能知道是个监听
2.监听的概念太生活了,即使非程序员也可能知道有些地方需要监听下。用其他方法实现监听虽然也能实现,但不一定能马上想到,使用起来信手拈来。
//1.0.0新扩展的俩个监听方法:listenTo和listenToOnce var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; //调用underscore.js的each方法遍历listenMethods,并向Events中添加俩个扩展的方法:listenTo和listenToOnce _.each(listenMethods, function(implementation, method) { //implementation为'on'或'once' //method为listenTo或listenToOnce //下面就是为Events添加listenTo和listenToOnce Events[method] = function(obj, name, callback) { //参数obj为被监听的对象 //参数name为被监听的事件 //参数callback为被监听的事件触发时执行的回调函数 var listeners = this._listeners || (this._listeners = {});//初始化个监听列表,存放该对象的所有监听 //为被监听对象分配个唯一的监听id(listenerId) var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); //依照监听id存入到监听列表中 listeners[id] = obj; //如果name是个对象({change: callback}),那么此时回调函数已经由对象的属性值担任, //所以形式参数callback应该是个上下文,并且上下文应是this,即callback = this if (typeof name === 'object') callback = this; //下面这句是监听的核心 //调用被监听对象的'on'或'once'绑定事件 obj[implementation](name, callback, this); return this; }; });
停止监听(stopListening)
stopListening: function(obj, name, callback) { var listeners = this._listeners; if (!listeners) return this; //判断是否要停止被监听对象的所有监听 //如果参数name和callback都未指定或都为null,则deleteListener为true,表示停止被监听对象的所有监听 var deleteListener = !name && !callback; //如果name是个对象({change: callback}),那么此时回调函数已经由对象的属性值担任, //所以形式参数callback应该是个上下文,并且上下文应是this,即callback = this if (typeof name === 'object') callback = this; //listeners中存放的是所有需要停止的监听 //如果obj为空时,则停止当前对象的所有监听 //如果obj不为空时,将listeners赋个空对象(listeners = {}),然后把由obj指定的被监听对象加入 if (obj) (listeners = {})[obj._listenerId] = obj; for (var id in listeners) { //调用被监听对象off方法移除事件 listeners[id].off(name, callback, this); if (deleteListener) delete this._listeners[id]; } return this; }
多事件处理(eventsApi)
我们知道当调用on或off来处理事件时,事件名可为多个。此时on或off方法会把处理权交给eventsApi,让eventsApi去处理。eventsApi会遍历事件名,然后逐个执行on或off方法。
分析下参数:
obj:事件对象,即那个要绑定(on)或移除(off)事件的对象
action:行为或方法名。即是"on"或“off”
name:事件列表。可以是个对象,此时对象属性名将作为事件名,属性值将作为回调方法。也可以是空白符链接的字符串。
rest:存放回调方法和上下文的数组,即[callback, context]
var eventsApi = function(obj, action, name, rest) { if (!name) return true; //判断事件名name的类型 //当指定的事件名是个对象,如:{change: callback} ,则将属性名(如:change)作为事件名,属性值作为回调函数 //逐个调用obj的on方法或off方法 if (typeof name === 'object') { for (var key in name) { //利用apply调用对象obj的"on"或"off"方法 //此时注意传给回调函数的参数,参数是[key, name[key]]与rest拼接后的数组。第1个是key(属性名),第2个是name[key](属性值),第3个是[callback, //context](此时只需要传入上下文即可) // obj[action].apply(obj, [key, name[key]].concat(rest)); } return false; } //当指定的事件名是以空白字符连接的字符串时,分割出每个事件,逐个调用obj的on方法或off方法 if (eventSplitter.test(name)) { //调用分割方法split var names = name.split(eventSplitter); for (var i = 0, l = names.length; i < l; i++) { obj[action].apply(obj, [names[i]].concat(rest)); } return false; } return true; }
为on和off起个别名
//为方法"on"和"off"在起个别名 Events.bind = Events.on; Events.unbind = Events.off;所以也可以通过bind方法绑定事件,通过unbind方法移除事件。
总结:对于Events来说多事件处理(eventsApi)方法是比较难理解的,尤其是对于事件名为对象或连接符链接的多个事件时,回调函数和上下文的变化需要弄清楚,否则尤其是在监听这块容易混淆。