有这么一段代码经常会出现在代码中
var pubsub = (()=>{
var topics = {};
function subscribe(topic,fn){
if(!topics[topic]){
topics[topic] = [];
}
topics[topic].push(fn);
}
function publish(topic,...args){
if(!topics[topic])
return;
for(let fn of topics[topic]){
fn(...args);
}
}
return {
subscribe,
publish
}
})()
测试代码
pubsub.subscribe('test',function(a,b){ //订阅者A订阅了test事件
console.log(a,b);
});
pubsub.publish('test','123','HH'); //123 HH(发布者B发布了test事件)
调用publish后打印出了123 HH。很奇妙的一段代码,当然实际上只是遍历了数组,然后把数组中的所有函数全部执行一遍而已。但是对于一个没读过实现代码的人来说,却是一个神奇的存在,JS居然能订阅发布消息,太酷了。
有的时候我会叫他观察者模式,有时候又会叫他发布订阅模式,觉得叫什么都是对的。
但是,他们并不一样。
差异
在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。发布订阅模式相比观察者模式多了个事件通道,订阅者和发布者不是直接关联的。
这段话可以看出上面的例子是发布订阅模式。订阅者A和发布者B是通过pubsub这个对象关联起来的,他们没有直接的交流。
那么真正的观察者模式是怎么样的?
一个或多个观察者对目标的状态感兴趣,通过将自己依附在目标对象上以便注册所感兴趣的内容。目标状态发生改变并且观察者可能对这些改变感兴趣,会发送一个通知消息,调用每个观察者的更新方法。当观察者不再对目标状态感兴趣时,他们可以简单将自己从中分离。
我们来实现下观察者模式。首先是目标的构造函数,他有个数组,用于添加观察者。还有个广播方法,遍历观察者数组后调用他们的update方法:
class Subject{
constructor(){
this.subs = [];
}
addSub(sub){
this.subs.push(sub);
}
notify(){
this.subs.forEach(sub=> {
sub.update();
});
}
}
那么观察者就得有个update方法:
class Observer{
update(){
console.log('update');
}
}
测试代码
let subject = new Subject();
let ob = new Observer();
//目标添加观察者了
subject.addSub(ob);
//目标发布消息调用观察者的更新方法了
subject.notify(); //update
可以看到目标和观察者是直接联系在一起的。观察者把自身添加到了目标对象中,可见和发布订阅模式差别还是很大的。在这种模式下,目标更像一个发布者,他让添加进来的所有观察者都执行了update函数,而观察者就像一个订阅者。
优劣
个人觉得发布/订阅模式比较简单,使用的也比较广泛。由于他订阅者和发布者不直接关联的特点我们完全可以把管理事件的对象写到一个单独文件中,作为库来使用。发布订阅模式中,双方不知道对方的存在,而观察者模式中,目标和观察者是直接联系起来的。具体选择什么模式,得视场景而定。一般来说,发布/订阅就够用了,简单清晰,符合我们dom事件编程的观念。
观察者模式哪里被用到过?vue的双向绑定。下篇就讲一下观察者模式在vue双向绑定实现中的运用。