-
Notifications
You must be signed in to change notification settings - Fork 11
Description
观察者模式--自定义事件模型详解
缘由
自定义事件相当灵活,用途相当广,在攻城湿界备受恩宠。今天大苹果尝试着实现了一个简易的自定义事件模型,测试地址请围观:
http://htmlhub.doorder.com/events/events.html
代码结构相当简单,思路也相当清晰,就缺拍砖的了。
我们的自定义事件对象有个高端大气上档次的名字:Events!
分析
Events应该提供一个内部列表(list)来保存被绑定的各种事件类型(以下称type)跟事件(以下称handler)等数据。
现实是:对于同一种type,有可能会被绑定多个handler,因此,每种type对应的是一个由其绑定的1个或多个handler组成的数组。
Events内部的list的数据结构是如下格式:
list = {
'type1' : [handler1],
'type2' : [handler2, handler3]
'type3' : [handler4, handler5, handler6 ...],
...
}简单的自定义事件模型应包括绑定、解绑和触发三个基本功能。根据以上事实,可以分别将以上三种功能分别称为 on、off和trigger。
架构
首先定义一个叫ev的类,并初始化它的list对象。
function ev(){
this.list = {} // 初始化list
}在它的原型prototype上设置以上三个方法。即:
ev.prototype = {
on : function(){
},
off : function(){
},
trigger : function(){
},
once : function(){
}
}注意到我还加上了once方法,它的功能是绑定一次性事件,当触发一次之后马上解绑该事件,由于它有着“阅后即焚”的隐私保护功能,在攻城湿界也有着巨大的市场份额。
在最外面用一个立即执行函数来包裹以上代码,最后返回这个ev类,交给外面早已支好锅等着的Events:
var Events = function(){
function ev(){
// ...
}
ev.prototype = {
// ...
}
return ev
}()此时的Events是全局对象,如果不产生全局对象,可以挂在jQuery对象下:
$.Events = function(){
//...
}()大结构很清晰了,下面依次实现功能吧。
实现
on方法:
on方法要接受两个参数,一个是type,另一个是handler,如果是首次绑定该type,需要初始化这个type的数组(见上面的数据结构),于是on方法可以这么实现:
ev.prototype.on = function(type, handler) {
if(typeof handler != 'function'){ // 确保handler是货真价实的函数
throw new Error('invalid handler')
}
this.list[type] = this.list[type] || [] // 起码是个数组
this.list[type].push(handler) // 因为数组,所以装货
return this // 返回自身,准备好菊花
}off方法:
off方法也接受两个参数type和handler,但它的不同之处在于,当两个参数都传递的时候,表示要解绑掉某个type下的某个handler;当只传递一个type的时候,表示要解绑掉某个type下的所有handler;而当不传递参数时,表示要解绑所有type的所有handler。因此首先要判断参数个数:
ev.prototype.off = function(type, handler) {
var len = arguments.length // 缓存参数个数
var list = this.list[type] // 缓存此type类型的数组
if(!len){ // 一个参数都没提供
this.list = {} // 直接清空list
} else if(len == 1) { // 有type,没handler
delete this.list[type] // 这个type的handler全清空
} else if(list) { // 有type,也有handler,也有list
for(len = 0; len < list.length; len ++){ // 地毯式扫荡handler
if(list[len] === handler) list.splice(len, 1) // 犀利的劈掉handler
}
}
return this // 照样菊花灿烂
}其中,list.splice(index, num, add)从数组list的第index个元素起,删除掉num个元素,再插上add等元素。
trigger方法:
trigger方法只需提供要触发的type即可,简单起见,将函数执行context上下文一律设置为window。此外,还需要转发自定义事件的任意参数。
ev.prototype.trigger = function (type){
var list = this.list[type] || [] // 如果不存在此type的话,就用空数组来充数。
var args = list.slice.call(arguments, 1) // 借用slice方法劈去type,剩下的都是人家自定义事件的
for(var i = 0, len = list.length; i < len; i ++){
list[i].apply(null, args) // 挨个触发事件,转发参数
}
return this // 菊花灿烂
}once方法:
once方法好难产,参考了jQuery的one方法,说多了都是泪!根据原理,绑定事件后,一旦触发此事件,就马上解绑!那怎么实现这样的功能呢?像下面这样,岂不是自欺欺人?
ev.prototype.once = function (type, handler) {
return this.on(type, handler).off(type, handler) // 绑定+解绑
}这样根本就不能触发了,还没触发该事件,就已经被暗杀了。经过搜索研究,发现可以处理下要传给on方法的handler,构造一个人肉炸弹one送给on,one一旦被触发,就会自爆:
ev.prototype.once = function (type, handler) {
if(typeof handler != 'function'){
throw new Error('invalid handler.')
}
var that = this // 缓存this对象
var one = function(){ // one里调用handler
handler.apply(null, arguments) // 所有参数任意转发
that.off(type, one) // 解绑one事件,注意解绑的是one!
}
return this.on(type, one) // 绑定one事件,还照样能提供菊花
}总结
于是,全部代码就形成了。
var Events = function() {
function ev(){
this.list = {} // 初始化list
}
ev.prototype = {
on : function(type, handler) {
if(typeof handler != 'function'){ // 确保handler是货真价实的函数
throw new Error('invalid handler')
}
this.list[type] = this.list[type] || [] // 起码是个数组
this.list[type].push(handler) // 因为数组,所以装货
return this // 返回自身,准备好菊花
},
off : function(type, handler) {
var len = arguments.length // 缓存参数个数
var list = this.list[type] // 缓存此type类型的数组
if(!len){ // 一个参数都没提供
this.list = {} // 直接清空list
} else if(len == 1) { // 有type,没handler
delete this.list[type] // 这个type的handler全清空
} else if(list) { // 有type,也有handler,也有list
for(len = 0; len < list.length; len ++){ // 地毯式扫荡handler
if(list[len] === handler) list.splice(len, 1) // 犀利的劈掉handler
}
}
return this // 照样菊花灿烂
},
trigger : function (type){
var list = this.list[type] || [] // 如果不存在此type的话,就用空数组来充个数。
var args = list.slice.call(arguments, 1) // 借用slice方法劈去type,剩下的都是人家自定义事件的
for(var i = 0, len = list.length; i < len; i ++){
list[i].apply(null, args) // 挨个触发事件,转发参数
}
return this // 菊花灿烂
},
once : function (type, handler) {
if(typeof handler != 'function'){
throw new Error('invalid handler.')
}
var that = this // 缓存this对象
var one = function(){ // one里调用handler
handler.apply(null, arguments) // 所有参数任意转发
that.off(type, one) // 解绑one事件,注意解绑的是one!
}
return this.on(type, one) // 绑定one事件,还照样能提供菊花
}
}
return ev
}()用法:
直接实例化一个Events:
var aaa = new Events
aaa.on('someEvent', function(){
console.log('custom event triggered.');
})
aaa.trigger('someEvent') // custom event triggered.
aaa.on('someEvent', function(arg){
console.log('custom event with args :' + arg)
})
aaa.trigger('someEvent', 'hello world.')
// custom event triggered.
// custom event with args :hello world.
var bbb = new Events
bbb.once('onceEvent', function(num){
console.log('this event will only trigger once: ' + num)
})
bbb.trigger('onceEvent', 1) // this event will only trigger once: 1
bbb.trigger('onceEvent', 2) // 不再执行!注意事项
在解绑off时,我们这么写:
var ccc = new Events
ccc.on('someEvent', function(){
console.log('someEvent occured!')
}).off('someEvent', function(){
console.log('someEvent occured!')
}).trigger('someEvent') // someEvent occured!WTF! 说好的解绑呢?还是解绑根本就是闹着玩的?
这里涉及到了对象比较问题。两个内容完全相同的对象也不会相等。
var p1 = {
name : 'lilei',
age : 12
}
var p2 = {
name : 'lilei',
age : 12
}
console.log(p1 === p2) // false函数就是对象。两个内容完全相同的函数也不会完全相等(list[len] === handler)。所以,当绑定事件时,如果此事件以后会涉及到解绑,则在开始就需要将其函数名或者函数引用传递给on跟off,而不是两个内容相同的函数字面量。
var ddd = new Events
var handler = function(num){
console.log('someEvent occured!' + num)
}
ddd.on('someEvent', handler).trigger('someEvent', 1) // someEvent occured!1
ddd.off('someEvent', handler).trigger('someEvent', 2) // 不再执行!