JavaScript设计模式·自己实现一个发布订阅的事件池
2020-02-21 本文已影响0人
oneSailboat
发布订阅模式是JavaScript常见的模式之一,他的根本思想在于:
通过一个事件池给元素的触发事件绑定多个方法(方法不可重复),后续扩展时只需往事件池中添加方法即可
下面给出一个jquery的事件池使用案例,方便理解
(ps: 每段代码“++++”中间)
// 初始化一个事件池
let pond = $.Callbacks();
// 在某个触发方法中启动事件池,再方法触发时会按顺序执行事件池中的方法
$("#button").click(function(){
// fire函数可带有参数,参数会传递给事件池中的所有函数
pond.fire();
});
let f1 = function(){console.log(1)};
let f2 = function(){console.log(2)};
let f3 = function(){console.log(3)};
// 向事件池中添加方法
pond.add(f1);
pond.add(f2);
pond.add(f3);
// 删除方法
pond.remove(f2);
上述方法执行之后会依次输出1 3
。
基于上面的理解,我们可以汇总得出,事件池需要有以下方法:
- 通过一个函数(
Callbacks
)进行初始化 - 通过一个函数添加到某个方法的执行体中(
fire
) - 可以按顺序添加(
add
)和删除(remove
)
接下来我们自己写一个发布订阅模式的事件池类,摆脱对jquery的依赖
- 首先形成闭包,放置变量污染
let _subscribe = function(){
}();
- 创建一个发布订阅类
Sub
类,并实现构造函数
let _subscribe = function(){
//+++++++++++++++++
class Sub{
// 构造函数:初始化一个函数数组
constructor(){
this.pond = [];
}
}
//+++++++++++++++++
}();
- 实现添加方法和删除方法
let _subscribe = function(){
class Sub{
// 构造函数:初始化一个函数数组
constructor(){
this.pond = [];
}
//+++++++++++++++++
// 添加函数
add(func){
// 判断是否是“函数”
if(typeof func !== "function"){
return;
}
// 判断是否与已经添加的方法重复
let flag = this.pond.some(item => item===func);
!flag?this.pond.push(func):null;
}
// 删除函数
remove(func){
for(let i=0;i<pond.length;i++){
let item = pond[i];
if(item===func){
pond[i] = null;
break;
}
}
}
//+++++++++++++++++
}
}();
这里有一些需要注意的地方:
-
add
中需要做两层检测:类型检测、重复检测 -
remove
为什么不用splice
?splice
会导致一种很隐秘的数组塌陷:当添加的方法中自带对其他方法的删除时会导致数组塌陷,结合下方的fire
便知
- 实现点燃函数,使函数依次执行
let _subscribe = function(){
class Sub{
// 构造函数:初始化一个函数数组
constructor(){
this.pond = [];
}
// 添加函数
add(func){
// 判断是否是“函数”
if(typeof func !== "function"){
return;
}
// 判断是否与已经添加的方法重复
let flag = this.pond.some(item => item===func);
!flag?this.pond.push(func):null;
}
// 删除函数
remove(func){
for(let i=0;i<pond.length;i++){
let item = pond[i];
if(item===func){
pond[i] = null;
break;
}
}
}
//+++++++++++++++++
// 点燃函数 args用于接收参数
fire(...args){
let pond = this.pond;
for (let i = 0; i < pond.length; i++){
let item = pond[i];
if (typeof item !== "function") {
// 此时再删除
pond.splice(i, 1);
i--;
continue;
}
// 在三个参数以上的情况下,call的性能略优于apply
item.call(this, ...args);
}
}
//+++++++++++++++++
}
}();
在fire
函数中将之前被remove
的方法删除,这样就不会造成数组塌陷问题
在绑定this
和参数时有一个小细节,【用call
而不用apply
】
这是因为call
在参数有3个以上时性能优于apply
,为什么呢?
因为apply
其实也要在最后调用call
,但是在调用之前要做一系列对数组格式的校验,这些校验的时间会造成性能的浪费
- 将调用方法暴露在闭包外
这里我们不需要用户通过构造函数重建一个对象,而是在闭包构造对象,然后让外界使用内部对象的方法,具体写起来只需要return
一个函数方法即可
let _subscribe = function(){
class Sub{
// 构造函数:初始化一个函数数组
constructor(){
this.pond = [];
}
// 添加函数
add(func){
// 判断是否是“函数”
if(typeof func !== "function"){
return;
}
// 判断是否与已经添加的方法重复
let flag = this.pond.some(item => item===func);
!flag?this.pond.push(func):null;
}
// 删除函数
remove(func){
for(let i=0;i<pond.length;i++){
let item = pond[i];
if(item===func){
pond[i] = null;
break;
}
}
}
// 点燃函数 args用于接收参数
fire(...args){
let pond = this.pond;
for (let i = 0; i < pond.length; i++){
let item = pond[i];
if (typeof item !== "function") {
// 此时再删除
pond.splice(i, 1);
i--;
continue;
}
// 在三个参数以上的情况下,call的性能略优于apply
item.call(this, ...args);
}
}
}
//+++++++++++++++++
// 暴露一个方法
return function subscribe(){
return new Sub();
}
//+++++++++++++++++
}();
这里可能被闭包来闭包去给绕晕,解释一下
- 这段代码最上面的
_subscribe
最终会赋值这个大闭包return的对象,也就是返回一个在闭包作用域中生成的对象地址 - 这个对象地址是一个方法,也就是最后的
subscribe
函数 - 我们在使用的时候会执行下列语句
let pond = _subscribe();
这句话做了个啥?
把_subscribe
执行的结果返回给pond
,也就是说其实pond
被赋了一个闭包中的闭包里存放的对象地址
More Idea?请关注个人博客 syfless
参考资料
- 珠峰前端相关教程
- 【 js 基础 】为什么 call 比 apply 快?