看到了一个开源的Events模块 pubsub.js,源代码大概三百多行,本来以为是很简单的一个工具类,但细看之下,发现代码中还是有很多晦涩之处,今天使用ES6将源码重构一遍,其中还是有很多值得学习的地方,不得不说,一个开源的模块要考虑的东西还是很多很多的。
先来看一下这个模块的特殊用法(常用的pubsub就不列举了)
//取消订阅 |
还有命名空间的通配符匹配*
,异步事件配置async
, 命名空间深度限制 depth
, 这几个功能在下面的实现中省略了,因为感觉用(tai)处(lan)很(le)少
按照这个Events模块的文档(他叫pubsub.js,我习惯了jQuery的事件模型和NodeJs的事件命名,这里我的命名为Events模块),我们先来建立一个TodoList
TODO
- 事件名可以添加命名空间,如
parent.child1.child2
- 自定义命名空间的分隔符,如:
'.'
、'/'
- 事件名可以继承,如
parent.child1.child2
除了可以触发本身的事件之外,还可以触发父级parent.child1
与parent
注册的事件,默认不可继承 - 指定回调函数的上下文
context
, 可以全局配置也可以给回调单独配置。 - 可注册
one
事件,注册后,只能执行一次 - 回调函数可以是数组,一个事件可触发多个回调
- 事件名可以是数组,多个事件名可以注册同一个回调或多个回调
- 运行环境兼容处理:node / require / browser
类的结构
首先我需要有以下几个常用方法
on()
用于注册事件off()
用于注销事件emit()
用于触发事件one()
用于注册一次性事件
那么这个类的结构如下
class Events{ |
为这个这个类指定有两个属性
cache
用于存储注册过的事件信息, 如果有命名空间,它将是一个树状结构的对象。options
类的配置separator
用于指定命名空间的分隔符,默认为.
号符inherit
事件名是否可以继承,默认不可以context
用于指定上下文,默认为设置为回调函数本身
一个一个Todo的来看
命名空间与分隔符
关于分隔符的处理,我们只要在 处理命名空间分隔字符串的时候,使用属性opttions中的配置就可以了,使用配置代替,不写死就OK。
事件名可以添加命名空间,如 parent.child1.child2
首先如果我们来看数据结构,如果不加命名空间,那么数据结构可以设计成这样
cache = { |
这样设计的好处就是可以给 eventName
这个事件,添加多个回调,而且可以为每一个回调指定上下文
那如果有命名空间的情况下怎么办呢?因为是层级关系,这就需要嵌套了cache = {
'eventName':{
events:[{fn:fn1, context: null}],
'child1':{
events:[{fn:fn1, context: null}],
'child2':{
events:[{fn:fn1, context: null}]
//....childn
}
}
}
}
这样就实现了命名空间下的事件存储关系,那么下一步,就是怎样将有命名空间的事件存到 cache
里去了
这一步操作显然是在我们注册事件时完成的,那么就来看这个 on()
方法吧
以这种实际中的用法为例event.on('parent.child1.child2',function(){
console.log(this.name)
},{
context: {name: 'jack'}
})
先来看看,怎么样根据命名空间生成对象树呢?
这里的方法是,把 key
按指定的分隔符分隔成数组,遍历数组,为每个事件对象添加 events
属性,属性的值是 {}
,遍历完成后,最终将回调函数添加进 child2
的 events
对象中
var cache = {}; |
这里利用了引用的传递的特点,last
本身是引用的 cache
,遍历完成,cache
中存的是完整树,而 last
中存的是最后一个叶子节点,因为是引用传递,last
中的数据操作会传递到 cache
中, 这种利用引用类型修改数据的特点在后面还会有更多的运用。
on()
函数的实现如下on(key, fn, config={}){
//先将key用配置的分隔符,分隔成数组 ['parent', 'child1', 'child2']
const keys = key.split(this.options.separator);
//获取上下文 优先级:自定义配置 > 全局配置 > 回调本身
const context = config.context || this.options.context || fn;
//获取已有的事件信息缓存
let keyObj = this.cache;//引用cache对象
// 这个对象记录了回调与回调的this
const eventObj = {fn: fn, context: context};
//这一步很关键,它将为我们创建一棵树,用于存储事件相关信息
keys.forEach(v=>{
if(!keyObj[v]){
keyObj[v] = {};
keyObj[v]['events'] = [];
}
keyObj = keyObj[v];
})
//经过上面的遍历,这里已经定位到了最里层的,给最终于的事件添加回调信息对象
keyObj.events.push(eventObj);
//这里返回的对象,用于注销方法`off()`使用
return {
namespace : key,
event : eventObj
};
}
经过on()
方法的运行,成功解析了有命名空间的事件注册,并将数据转成了树结构的对象缓存到了 cache
中
事件注册转成数据存储之后,后面就是触发操作,接来处理一下触发 emit()
方法
实际调用events.emit('parent.child1.child2')
这里需要处理的是,第一,是否考虑继承,如果不考虑,那么直接触发 child2
的回调,如果考虑,则应该先触发parent
的回调,再触发child1
的回调,最后触发child2
的回调
emit(key, args, config={}){ |
off()
方法的处理,参数是 on()
方法的返回值,由事件名和事件对象组成的对象,off()
方法是注销事件或叫删除事件,无论是有命名空间,还是没有命名空间,我们都应该注销最内层的事件就可以了,所以这里处理比较简单
off(obj){ |
one的实现
经过这三个主方法的处理,就基本上完成一大半了,再来看一下我们的TODOX 事件名可以添加命名空间,如 `parent.child1.child2`
X 自定义命名空间的分隔符,如:`'.'` 、 `'/'`
X 事件名可以继承,如 `parent.child1.child2` 除了可以触发本身的事件之外,还可以触发父级 `parent.child1` 与 `parent` 注册的事件,默认不可继承
X 指定回调函数的上下文 `context`, 可以全局配置也可以给回调单独配置。
5. 可注册 `one` 事件,注册后,只能执行一次
6. 回调函数可以是数组,一个事件可触发多个回调
7. 事件名可以是数组,多个事件名可以注册同一个回调或多个回调
8. 运行环境兼容处理:node / require / browser
继承和指定上下文其实上面的代码已经处理了,继承时遍历每一个层级的回调数组进行触发,触发时,使用apply使用了在 on()
方法传递的 context
参数进行了this
指定。
好了,只剩下 5
、6
、7
、8
了
先来看看 one()
方法,这个方法用于注册一次性事件,触发一次后就不能再触发了,使用方式如下
event.one('cus',function(name){ |
这里实现的方法可以是这样:在注册 one()
事件时,将回调使用一个匿名函数包装一层,用这个函数代替回调,并在这函数内添加注销这个事件的操作,具体如下:
one(key, fn, config){ |
事件名与回调数组
再看 6
和7
处理事件名数组与回调数组
先看看应用场景
events.on(['cus1','cus2'], [function(text) { |
这里相当于是一个笛卡尔乘积,[1,2]x[3,4]=>[[1,3],[1,4],[2,3],[2,4]]
,多个事件名,每一个事件名对应多个回调。
这里的处理应该是这个模块最难的地方,关于算法,我并不擅长,pubsub.js
的作者使用了两个交替数组遍历加递归的处理方式,感受一下
on()
函数改造如下,这里添加了一个 _register
用于递归时返回数据。
on(keys, fns, config={}){ |
这里的执行顺序比较绕,还是用一张图来看一下
- 判断
fns
是数组,进到1
中,递归on()
方法,回调数组变成第一个回调 - 判断
keys
是数组, 进到2
中,递归on()
方法,此时keys
和fns
都不再是数组 - 进到
3
中,调用_register
注册事件,并返回结果,回到3
中,继续遍历keys
,调用on()
方法 - 进到
3
中,返回结果后,回到3
中,keys
遍历结束,进到4
中,返回存有两个事件对象的res
- 回到
1
中,继续遍历fns
,又是一个循环,2->3->2->3->4
,又返回了存有两个事件对象的res
- 最后
fns
遍历结束,res.concat(res)
之后,res
就存储了四个事件对象,再最后到4
, 返回结果res
至此功能基本完成,再来看一下TODO
X 事件名可以添加命名空间,如 `parent.child1.child2` |
环境兼容处理
最后一项很简单,至于browser,支持ES6的就自然兼容了
(function(scope){ |
OK,所有TODO都close掉了,这样具备了一个完整功能的事件模块可以尝试运用于项目中了,当然肯定还有一些小的bug或未实现的功能,这个留着在项目使用中慢慢去完善吧
另外,我们还需要写一个单元测试,看看是否所有功能都是正常可行的,留着下一次来实现吧,今天就到这里,休息,休息一下
完整代码
;(function(scope){ |
使用方式
var events = eventEmitter({inherit:true, context:{name:'jack'}}); |