目录
1、啥是发布订阅者模式
发布订阅者模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,一般用事件模型来代替传统的发布—订阅者模式。
在实际开发中,只要我们在 DOM 节点上绑定过事件函数,那就曾经使用过发布-订阅者模式,下面看个例子:
document.body.addEventListener('click',function(){
alert('你点击了页面');
});
document.body.click(); //模仿用户点击
这个例子中监控了用户点击 document.body 的动作,但是我们没有办法预知用户将在什么时候点击。所以我们订阅 document.body 上的 click 事件,当 body 被点击时,body 节点便会向订阅者发布消息。当然还可以随意的增加或删除订阅者,增加任何订阅者都不会影响发布者代码的编写。
2、jQuery 中的发布订阅
设想一个例子,当用户点击一个按钮时,依次执行绑定在按钮上的事件。在不使用发布订阅者模式的情况下,如果开发过程中有新的方法,则需要不仅需要手动把新的方法添加进去,还需要找到 click() 的位置。
针对上面的问题,jquery 的发布订阅者模式是,创建一个事件池:($.Callbacks()、add/remove、fire)
<button class='submit'>按钮</button>
<script src="../jquery.main.js"></script>
<script>
let fun1 = function(){
console.log(1);
};
let fun2 = function(){
console.log(2);
};
//=>创建一个事件池$.Callbacks()
let $pond = $.callbacks();
$('.submit').click(function(){
//=>点击的时候通知事件池中的方法执行,而且还可以给每个方法都传递实参
$pond.fire(100,200); //可以传参
});
$.pond.add(fun1);
$.pond.add(fun2);
let fun3 = function(m,n){ //如果再需要添加方法
console.log(m+n); //300
};
$pond.add(fun3); //直接通过add()就可以添加订阅者方法了
</script>
3、基于ES6封装发布订阅
(1)封装:上面这种方法依托 jquery 的库,但是真实项目中有可能基于 Vue、React 等框架,是不会用到 jquery 的,但是有时候还需要用到发布订阅这种思想,这个时候就需要自己封装一个发布订阅库。
class Subscribe{
constructor(){
this.pond = []; //创建一个事件池,用来存储后期需要执行的方法
}
add(func){ //向事件池中追加方法
let flag = this.pond.some(item => {
return item === func;
})
!flag ? this.pond.push(func) : null;
}
remove(func){ //从事件池中移除方法
let pond = this.pond;
for(let i=0;i<pond.length;i++){
let item = pond[i];
if(item === func){ //找到之后将其移除
//pond.splice(i,1); //不能这么写,会导致数组塌陷问题
pond[i] = null; //所以不能真移除,只能把当前项赋值为null
break;
}
}
}
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;
item.call(this,...args); //this此时指向Subscribe实例
}
}
}
(2)测试:上述代码就已经将发布订阅模式封装成了一个插件,保存到一个 .js 文件中,如果使用,直接在项目中引用即可。
<button class='submit'>按钮</button>
<script src="../subscribe.js"></script>
<script>
let pond = new Subscribe();
document.querySelector('.submit').onclick = function(event){
pond.fire(event); //执行事件池中的方法,并将事件对象传递进来
}
let fun1 = function(){
console.log(1);
};
let fun2 = function(){
console.log(2);
};
pond.add(fun1);
pond.add(fun1); //验证是否去重
pond.add(fun2);
let fun3 = function(event){
console.log(3,event);
};
pond.add(fun3);
</script>
4、数组塌陷问题
我们在封装发布订阅这模式的时候内部的事件池是用数组来代替的,但是在实际需求中可能会有这种情况:当依次执行事件池中的事件的时候,执行 fun2 的时候,它的操作是 remove 事件池中的 fun1 方法,这个时候就会导致数组塌陷问题,具体如下图:
因为要考虑到上面这种数组塌陷的情况,所以在封装发布订阅插件的时候,要移除某个方法就不能直接使用 splice() 方法,而是先将该方法所在的数组位置设置为 null,当执行 fire() 方法的时候,依次执行事件池中的函数,再循环判断数组当前位置上是否为函数,如果不是函数而是 null,再将其去除,然后将数组下标减一,这样就可以防止数组塌陷问题了。