觀察者模式和發布-訂閱模式有區別嗎?
在深入探討區別之前,我們先來分別討論下“觀察者模式”和“發布-訂閱模式”
# 觀察者模式
我認為大多數人都會同意觀察者模式是學起來最好入門的,因為你從字面意思就能知道它主要是做什么的。
> 觀察者模式在軟件設計中是一個對象,維護一個依賴列表,當任何狀態發生改變自動通知它們。
看吧,即使是維基百科的定義,也不是很難, 對吧? 如果你還不清楚,那讓我用通俗易懂的來解釋。
我們假設你正在找一份軟件工程師的工作,對“香蕉公司”很感興趣。所以你聯系了他們的HR,給了他你的聯系電話。他保證如果有任何職位空缺都會通知你。這里還有幾個候選人也你一樣很感興趣。所以職位空缺大家都會知道,如果你回應了他們的通知,他們就會聯系你面試。
所以,以上和“觀察者模式”有什么關系呢?這里的“香蕉公司”就是Subject,用來維護Observers(和你一樣的候選人),為某些event(比如職位空缺)來通知(notify)觀察者。
是不是很簡單!?

```
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),消息不會直接發送給特定的接收者(訂閱者)。
意思就是發布者和訂閱者不知道對方的存在。需要一個第三方組件,叫做信息中介,它將訂閱者和發布者串聯起來,它過濾和分配所有輸入的消息。換句話說,發布-訂閱模式用來處理不同系統組件的信息交流,即使這些組件不知道對方的存在。
那么如何過濾消息的呢?事實上這里有幾個過程,最流行的方法是:基于主題以及基于內容。好了,就此打住,如果你感興趣,可以去維基百科了解。

```
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)
```
# 區別
我用下圖表示這兩個模式最重要的區別:

我們把這些差異快速總結一下:
* 在觀察者模式中,觀察者是知道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]);
}
}
}
};
```