观察者模式和发布-订阅模式有区别吗?
在深入探讨区别之前,我们先来分别讨论下“观察者模式”和“发布-订阅模式”
# 观察者模式
我认为大多数人都会同意观察者模式是学起来最好入门的,因为你从字面意思就能知道它主要是做什么的。
> 观察者模式在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变自动通知它们。
看吧,即使是维基百科的定义,也不是很难, 对吧? 如果你还不清楚,那让我用通俗易懂的来解释。
我们假设你正在找一份软件工程师的工作,对“香蕉公司”很感兴趣。所以你联系了他们的HR,给了他你的联系电话。他保证如果有任何职位空缺都会通知你。这里还有几个候选人也你一样很感兴趣。所以职位空缺大家都会知道,如果你回应了他们的通知,他们就会联系你面试。
所以,以上和“观察者模式”有什么关系呢?这里的“香蕉公司”就是Subject,用来维护Observers(和你一样的候选人),为某些event(比如职位空缺)来通知(notify)观察者。
是不是很简单!?
![](https://box.kancloud.cn/ccf0461227a80d6c5b02f85762b33dae_1339x497.png)
```
class Subject {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
class Observer {
update() {
console.log('update')
}
}
let subject = new Subject()
let observer = new Observer()
subject.addSub(observer)
subject.notify()
```
# 发布-订阅模式
在观察者模式中的Subject就像一个发布者(Publisher),而观察者(Observer)完全可以看作一个订阅者(Subscriber)。subject通知观察者时,就像一个发布者通知他的订阅者。这也就是为什么很多书和文章使用“发布-订阅”概念来解释观察者设计模式。但是这里还有另外一个流行的模式叫做发布-订阅设计模式。它的概念和观察者模式非常类似。最大的区别是:
> 在发布-订阅模式中,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者(订阅者)。
意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。
那么如何过滤消息的呢?事实上这里有几个过程,最流行的方法是:基于主题以及基于内容。好了,就此打住,如果你感兴趣,可以去维基百科了解。
![](https://box.kancloud.cn/5554cb71a2f07989b5b279d29e5fe2c2_450x266.png)
```
let watcher = (() => {
let handlers = {}
function sub(name, callback) {
if (!handlers[name]) {
handlers[name] = []
}
handlers[name].push(callback)
}
function pub(name, ...args) {
if (!handlers[name]) {
return
}
handlers[name].forEach(fn => {
fn(...args)
})
}
return {
sub,
pub
}
})()
watcher.sub('test', (a, b) => {
console.log(a, b)
})
watcher.sub('test', a => {
console.log(a)
})
watcher.pub('test', 1, 2)
```
# 区别
我用下图表示这两个模式最重要的区别:
![](https://box.kancloud.cn/67ae957155de3afb10852e350fce5255_512x406.png)
我们把这些差异快速总结一下:
* 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
* 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
* 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。
* 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。
尽管它们之间有区别,但有些人可能会说发布-订阅模式是观察者模式的变异,因为它们概念上是相似的。
# 测试题
请实现下面的自定义事件 Event 对象的接口,功能见注释(测试1)
该 Event 对象的接口需要能被其他对象拓展复用(测试2)
```
// 测试1
Event.on('test', function (result) {
console.log(result);
});
Event.on('test', function () {
console.log('test');
});
Event.emit('test', 'hello world'); // 输出 'hello world' 和 'test'
// 测试2
var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
console.log('person1');
});
person2.on('call2', function () {
console.log('person2');
});
person1.emit('call1'); // 输出 'person1'
person1.emit('call2'); // 没有输出
person2.emit('call1'); // 没有输出
person2.emit('call2'); // 输出 'person2'
var Event = {
// 通过on接口监听事件eventName
// 如果事件eventName被触发,则执行callback回调函数
on: function (eventName, callback) {
//你的代码
},
// 触发事件 eventName
emit: function (eventName) {
//你的代码
}
};
```
先来做测试1,通过观察,这就是上面所讲的发布-订阅模式,于是DuangDuangDuang的敲代码吧
```
let Event = (() => {
let handlers = {}
function on(eventName, callback) {
if (!handlers[eventName]) {
handlers[eventName] = []
}
handlers[eventName].push(callback)
}
function emit(eventName, ...args) {
if (handlers[eventName] && handlers[eventName].length > 0) {
handlers[eventName].forEach(callback => {
callback(...args)
})
}
}
return {
on,
emit
}
})()
```
完美通过测试1,接下来看测试2
测试2中有这样一段代码:`Object.assign()`,我们先来看看这是干嘛的。
> `Object.assign(target, ...sources)`这个是ES6的新对象方法,用于对象的合并,将源对象(source)的所有**可枚举**属性,复制到目标对象(target)
Object.assign()接口可以接收多个参数,第一个参数是目标对象,后面的都是源对象,assign方法将多个原对象的属性和方法都合并到了目标对象上面,如果在这个过程中出现同名的属性(方法),后合并的属性(方法)会覆盖之前的同名属性(方法)。
Object.assign()是浅拷贝,对于引用类型的数据,拷贝的是其引用,不会具体的值,举个例子:
```
let a = {
b: {
c: 3
}
}
let d = Object.assign({}, a)
d.b.c = 5
console.log(a.b.c) // 5
```
但是,需要注意的一点是,对象第一层的引用类型属性是深拷贝
```
let d = Object.assign({}, a)
d.b = {
e: 6
}
console.log(a.b) // {c: 3}
```
接下来我们直接测试上面的代码
```
person1.emit('call1') // person1
person1.emit('call2') //
person2.emit('call1') //
person2.emit('call2') // person2
```
OK 完美通过,但是看到网上有些文章的测试结果却是下面这样:
```
person1.emit('call1') // person1
person1.emit('call2') // person2
person2.emit('call1') // person1
person2.emit('call2') // person2
```
出现这个结果的原因可能是assign方法之前版本都是浅拷贝,包括第一层属性,而目前版本已经修改了这个问题吧!!
但是浅拷贝为什么会出现上面的结果呢?原因就是如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。由于进行测试一的时候调用了on方法,所以event里面已经有了handles这个可枚举的属性。然后再分别合并到两个person里面的话,两个person对象里面的handles都只是一个引用。所以就互相影响了。
所以,终极完美解决办法就是:摒弃浅拷贝,实现深拷贝。但是题目已经固定必须使用assign方法,那么从另一方面入手:我们将handles这个属性定义为**不可枚举**的,然后在person调用on方法的时候再分别产生handles这个对象。
```
var Event = {
// 通过on接口监听事件eventName
// 如果事件eventName被触发,则执行callback回调函数
on: function (eventName, callback) {
//你的代码
if(!this.handles){
//this.handles={};
Object.defineProperty(this, "handles", {
value: {},
enumerable: false,
configurable: true,
writable: true
})
}
if(!this.handles[eventName]){
this.handles[eventName]=[];
}
this.handles[eventName].push(callback);
},
// 触发事件 eventName
emit: function (eventName) {
//你的代码
if(this.handles[arguments[0]]){
for(var i=0;i<this.handles[arguments[0]].length;i++){
this.handles[arguments[0]][i](arguments[1]);
}
}
}
};
```