觀察者模式和發(fā)布訂閱模式
前言
程序的設(shè)計(jì)模式有很多種,想必大家已經(jīng)不陌生了。
最近有小伙伴問我觀察者模式和發(fā)布訂閱模式有什么區(qū)別?
我內(nèi)心OS:觀察者模式和發(fā)布訂閱不就是換了個(gè)說法嗎?引用一下《Head First設(shè)計(jì)模式》里講的:
Publishers + Subscribers = Observer Pattern
然后小伙伴說,這不對(duì)呀,好多帖子都說是不一樣的??!
我也就帶著這樣的好奇心去查閱了一些文檔資料,總算是徹底搞明白了這兩個(gè)模式到底有什么區(qū)別,廢話不多說,看官老爺聽我娓娓道來,保證看完你會(huì)明白。
觀察者模式
首先,本著解釋一件復(fù)雜事物的時(shí)候一定不能引入新的概念的原則,我先來舉個(gè)生活中的例子告訴大家什么是觀察者模式。
我和小張同時(shí)寫了一個(gè)游戲項(xiàng)目,但是分工不同,我獨(dú)自完成了Map,Sound兩個(gè)模塊,小張寫的是一個(gè)image加載模塊。
需求是要在圖片加載完成后,才進(jìn)行加載Map和Sound,也就是執(zhí)行以下Map.init()和Sound.init()這兩個(gè)函數(shù),于是我需要告訴小張把這兩個(gè)函數(shù)加進(jìn)去。
小張此時(shí) :
imageLoad(images, function(){
Map.init()
Sound.init()
})
功能一切正常,有一天新加了個(gè)需求,需要在圖片加載完,加載一些活動(dòng)充值的展示,于是我又開發(fā)了一個(gè) Activity.init(),告訴小張,發(fā)現(xiàn)小張請(qǐng)假了。。。
此時(shí)我得找到小張的代碼,看到上面的圖片加載函數(shù),但是我不敢輕舉妄動(dòng)啊,萬一出問題呢,于是我就打電話給小張,說這個(gè)Activity.init()能放在之前的加載函數(shù)內(nèi)嗎,小張(內(nèi)心OS:淦?。┆q豫了一下,說我看看啊,然后小張?jiān)诙燃俅逄统鲭娔X,噼里啪啦半天告訴我,我來給你加吧,最后還是得靠小張。
上面這種就屬于一種代碼的耦合帶來的維護(hù)的不便利性。
那我們?cè)趺幢苊庖挥惺戮驼倚埬??此時(shí)就需要運(yùn)用到行為模式,讓程序自己說話,就不需要小張了,程序往外用大喇叭告訴我,我這ok啦,你們可以接班啦。
我們只需要接收這一個(gè)信號(hào)就可以了。
此時(shí)代碼可以寫成這樣,imageLoad().then(() => {}) 沒錯(cuò),你天天用的Promise.then也是一種發(fā)布訂閱的方式,只不過他是實(shí)現(xiàn)了一個(gè)nextTick去通知回調(diào)執(zhí)行,這個(gè)我們這里先不做展開。
或者寫成這樣:
imageLoad.on('success', () => { Map.init });
imageLoad.on('success', () => { Sound.init });
imageLoad.on('success', () => { Activity.init });
哇,這樣太好了,我們終于不用擔(dān)心小張?jiān)诓辉诹?,因?yàn)樾垖懙拇a已經(jīng)告訴我們執(zhí)行的狀態(tài),小張也不用擔(dān)心后面要加什么功能,只聚焦到自己的模塊上。
這就是觀察者模式或者發(fā)布訂閱模式,讓程序有一些行為,小張此時(shí)就是發(fā)布者,我在訂閱小張的消息*。
其實(shí)生活中很多這樣的例子:比如你去面試,HR告訴你說回去等通知吧,此時(shí)你給HR留下了自己的聯(lián)系方式就可以回去等待HR通知你面試的結(jié)果,這個(gè)時(shí)候你不用有事沒事就來問面試官。
你:結(jié)果咋樣啦?結(jié)果咋樣啦?結(jié)果咋樣啦?這個(gè)叫輪詢,是在HR不知道你的聯(lián)系方式時(shí)你去主動(dòng)聯(lián)系的,就好比服務(wù)器不知道每一個(gè)客戶端的身份,但是客戶端是可以知道服務(wù)器在哪的(好吧扯遠(yuǎn)了,輪詢我們以后再講)。
此時(shí)我們不需要去一直問面試官,只需要等HR打電話告訴你,這個(gè)時(shí)候你就是扮演著訂閱消息的觀察者,面試官扮演著發(fā)布消息的被觀察者,面試官管理者一大批的觀察者,等到出了面試結(jié)果,他統(tǒng)一去發(fā)通知給所有正在觀察或者訂閱面試是否成功這個(gè)消息的觀察者。
你只要給面試官一個(gè)聯(lián)系方式,發(fā)消息的權(quán)利在面試官身上。
上面應(yīng)該很好的解釋了什么是觀察者模式,那我們也能很清楚的分析得知,觀察者模式里面,notifyAllObservers()方法所在的實(shí)例對(duì)象,就是被觀察者(Subject,或者叫Observable),它只需維護(hù)一套觀察者(Observer)的集合,這些Observer實(shí)現(xiàn)相同的接口,Subject只需要知道,通知Observer時(shí),需要調(diào)用哪個(gè)統(tǒng)一方法就好了。

看完大體的設(shè)計(jì)架構(gòu),我們來通過程序看一下如何實(shí)現(xiàn)一個(gè)觀察者。
const observer = function () {
const events = {}
return {
on(callbackName, callback) {
if (events[callbackName]) {
events[callbackName].push(callback)
} else {
events[callbackName] = [callback]
}
},
emit(callbackName) {
events[callbackName].forEach(callback => callback())
},
remove(callbackName, callback) {
if (events[callbackName] && callback) {
events[callbackName] = events[callbackName].filter(preCallback => preCallback !== callback)
} else if (events[callbackName]) {
events[callbackName] = []
}
}
}
}
const ob = observer()
ob.on('hello', function () {
console.log('h')
})
ob.on('hello', function () {
console.log('e')
})
ob.on('hello', function () {
console.log('l')
})
ob.on('hello', function () {
console.log('l')
})
ob.on('hello', function () {
console.log('o')
})
ob.emit('hello')
我們可以看到,在上述代碼中,觀察者有序地往行為對(duì)象內(nèi)部注冊(cè)相同的"hello"事件,這些事件都被行為對(duì)象管理了起來,當(dāng)我們需要調(diào)用時(shí),行為對(duì)象就可以通過ob.emit('hello')觸發(fā)注冊(cè)的事件。
現(xiàn)在我們可隨意地添加發(fā)布訂閱的函數(shù)到行為對(duì)象(Subject也就是上述的ob)身上,此時(shí)發(fā)布和訂閱的通道是松耦合的,但是依然在ob內(nèi)部進(jìn)行管理,無法實(shí)現(xiàn)解耦。
上面的代碼實(shí)現(xiàn)了對(duì)事件的觀察者對(duì)象,那我們想像Vue或者React那樣,在狀態(tài)改變的同時(shí)也能實(shí)時(shí)通知依賴組件,這又該怎么做呢?
class PubSub {
constructor() {
this.state = 0;
this.observers = []
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
getState() {
return this.state
}
attach(ob) {
this.observers.push(ob)
}
notifyAllObservers() {
this.observers.forEach(ob => ob.update())
}
}
class Observer {
constructor(obName, sub) {
this.obName = obName
this.sub = sub
this.sub.attach(this)
}
update() {
console.log(`name: ${this.obName}, subState: ${this.sub.getState()}`)
}
}
const ss = new PubSub()
const obse1 = new Observer('ob1', ss)
const obse2 = new Observer('ob2', ss)
const obse3 = new Observer('ob3', ss);
(function () {
let stateNum = ss.getState()
let timer = setInterval(() => {
ss.setState(stateNum++)
if (stateNum > 10) {
clearInterval(timer)
timer = null
}
}, 1000)
})()
/*
所有的觀察者都會(huì)注入我們的PubSub實(shí)例,每生成實(shí)例的同時(shí),就會(huì)注冊(cè)一個(gè)observer交由Subject管理,從而實(shí)現(xiàn)狀態(tài)改變通知全部的觀察者對(duì)象。
*/
同學(xué)們不需要死記硬背,下面由我來用大白話講一下這套模式的內(nèi)功心法
發(fā)布者更改狀態(tài)要通知到所有的訂閱者身上,或者說是通知到所有的觀察對(duì)象身上
為什么可以通知到,是因?yàn)槲覀冃薷臓顟B(tài)的時(shí)候,調(diào)用了訂閱者的函數(shù),發(fā)布者那里必然留下了訂閱者的聯(lián)系方式,也就是這個(gè)訂閱者的函數(shù)
而訂閱者那里,什么都不需要管,那為什么只要發(fā)布就會(huì)通知到訂閱者那里呢,因?yàn)橛嗛喺咭恢钡胗浿l(fā)布者,所以訂閱者心里一定住著一個(gè)發(fā)布者,并且一定會(huì)給發(fā)布者留下自己的聯(lián)系方式,這是我們的思維核心
至于為什么像Vue那樣不需要setState就可以通知到組件,是因?yàn)閂ue2用了Object.defineProperty() 進(jìn)行觀察對(duì)象的節(jié)點(diǎn)變化進(jìn)行數(shù)據(jù)攔截,從而在內(nèi)部去執(zhí)行了setState()的相關(guān)操作
以上請(qǐng)仔細(xì)閱讀,理解了必然會(huì)弄清楚觀察者模式的行為邏輯。
發(fā)布訂閱模式
其實(shí)我認(rèn)為,發(fā)布訂閱模式不應(yīng)該拎出來說是一種設(shè)計(jì)模式。
模式都是按封裝目的歸類的,按意圖區(qū)分。
發(fā)布訂閱是一種廣義上的觀察者模式,其實(shí)不能說他和觀察者模式有什么不同,而是他作為觀察者模式的一個(gè)變種或者解耦方案,新增了一個(gè)消息中間件,為觀察者模式中的發(fā)布訂閱增加了一條消息的中間通道。
在觀察者模式中,觀察者或者說訂閱者需要直接訂閱目標(biāo)事件,而發(fā)布者可以直接發(fā)布一條觀察者或者訂閱者可以接受的消息。
觀察者模式:數(shù)據(jù)源直接通知訂閱者發(fā)生改變。
發(fā)布訂閱模式:數(shù)據(jù)源告訴第三方(事件頻道)發(fā)生了改變,第三方再通知訂閱者發(fā)生了改變。
在設(shè)計(jì)模式結(jié)構(gòu)上,發(fā)布訂閱模式繼承自觀察者模式,是觀察者模式的一種實(shí)現(xiàn)的變體。
在設(shè)計(jì)模式意圖上,兩者關(guān)注點(diǎn)不同,一個(gè)關(guān)心數(shù)據(jù)源,一個(gè)關(guān)心的是事件消息。

我們知道不管是觀察者還是發(fā)布訂閱,其實(shí)都是一種行為,這也是設(shè)計(jì)模式中的行為模式。這兩個(gè)模式的目的就是給模塊建立一條信息通道,方便模塊間的信息傳輸,只是手段和重點(diǎn)不一樣。
他們都解決了一個(gè)問題,對(duì)于多個(gè)不同對(duì)象基于同一個(gè)對(duì)象變化時(shí)需要同步自身狀態(tài)或者做一些操作時(shí),怎么能夠降低代碼的耦合程度。
-
舉個(gè)例子
就好比我們?cè)诰W(wǎng)上購物,之前是快遞員上門送貨,后來快遞太多了,為了增加效率,分工更明確一點(diǎn),現(xiàn)在多了個(gè)中間站,菜鳥驛站,快遞員方便了,這是在規(guī)模起來以后自然而然的選擇。
現(xiàn)在是人主動(dòng)去拿快遞,如果以后連這也嫌棄效率不高,怎么辦?
再加一層,菜鳥驛站派出無人機(jī)送,你品,這就是解耦以后可以干的事情。
例子我們就不多說了,我們關(guān)注一下代碼層面,發(fā)布訂閱的優(yōu)勢(shì)。
// 發(fā)布者只管發(fā)布消息,不管消息被誰獲取了,通常將消息發(fā)給平臺(tái)(消息中間件),讓平臺(tái)去分發(fā)消息
class PubLisher {
constructor(TopicChannel) {
this.channel = {}
this.channelList = []
this.addChannel(TopicChannel)
}
addChannel(channel) {
this.channelList.push(channel)
}
doA() {
this.publish('doA')
}
doB() {
this.publish('doB')
}
doC() {
this.publish('doC')
}
publish(msg) {
this.channelList.forEach(channel => channel.notifyMsg(msg))
}
}
// 消息中間件只管將發(fā)布者的消息處理成不同的channel供訂閱者去訂閱,維護(hù)的是訂閱不同topic的訂閱者列表,等待發(fā)布者一聲令下通知訂閱者們
class TopicChannel {
constructor(channelName) {
this.channelName = channelName
this.SubjectList = []
}
addSubject(subject) {
this.SubjectList.push(subject)
}
notifyMsg(msg) {
this.interceptPublishMsg(msg)
}
interceptPublishMsg(pubMsg) {
const msgBbj = {
'doA': 'doA',
'doB': 'doA',
'doC': 'doC'
}
this.notifyAllSubject(msgBbj[pubMsg])
}
notifyAllSubject(pubTopic) {
this.SubjectList.forEach(subject => subject[pubTopic]())
}
}
// 訂閱者只關(guān)心自己要做什么(事件),不關(guān)心是誰發(fā)布的 ,通常要在平臺(tái)注冊(cè)某個(gè)事件
class Subject {
constructor(subName, TopicChannel) {
this.subName = subName
this.TopicChannel = TopicChannel
this.TopicChannel.addSubject(this)
}
doA() {
console.log(this.subName + 'doA')
}
doB() {
console.log(this.subName + 'doB')
}
doC() {
console.log(this.subName + 'doC')
}
}
// 一個(gè)頻道T1
const T1 = new TopicChannel('T1')
// 三個(gè)訂閱者訂閱了T1頻道的消息
const S1 = new Subject('S1', T1)
const S2 = new Subject('S2', T1)
const S3 = new Subject('S3', T1)
// 一個(gè)發(fā)布者 發(fā)布消息到T1頻道
const P1 = new PubLisher(T1)
P1.doA()
P1.doB()
P1.doC()
你以為訂閱者完全按照你的指令去做事了,其實(shí)他們被中間商篡改了你發(fā)布的指令:

我們驚訝的發(fā)現(xiàn),每一個(gè)訂閱者S完全不需要認(rèn)識(shí)發(fā)布者P,發(fā)布者P也不需要認(rèn)識(shí)訂閱者S,發(fā)布者P只需要知道T可以幫他傳遞消息就可以,訂閱者S也只要知道T可以通知到自己辦事就行。
像不像生活中的租房者,房屋中介,房東,彼此不需要躍層認(rèn)識(shí),就可以完成房子租約。
你就是訂閱者不需要找到房東你只要說出自己需要多大的房子朝向如何多少租金等需求。
中介會(huì)在自己的體系中為你匹配,你只需要等中介通知你就行。
中介作為消息中間件只需要維護(hù)租房者和房東們,與房東簽署了協(xié)議房東說這個(gè)可以租你租3000吧,中介為了賺中介費(fèi)就通知到了租房者們說4000。
房東作為發(fā)布者,只要給平臺(tái)和中介發(fā)自己的房子的基本信息,不需要找到租房者,中介給他錢就行。
上面的例子我們可以拓展出很多租房者,發(fā)布者,多平臺(tái),這就是你平時(shí)看程序設(shè)計(jì)說的松散耦合帶來的程序拓展性可插拔性的優(yōu)勢(shì)。
其實(shí)像這種中介一樣的模式,在程序里叫做經(jīng)紀(jì)人模式或者代理模式,ES6中有Proxy實(shí)現(xiàn)了代理,Vue3中也是運(yùn)用了Proxy替換了之前使用的Object.defineProperty,我們來看一下Proxy。
Proxy
關(guān)于他的定義我們就不多說了,網(wǎng)上真的太多了,可以去MDN上看。
我們這里只用一個(gè)例子來解釋一下什么是代理,以及代理能干什么。
// 以下純屬虛構(gòu)
// 這是古代皇帝要賑災(zāi)的實(shí)際款項(xiàng)
const Project = {
wood: '10w兩白銀',
rice: '20w兩白銀',
silks: '30w兩白銀'
}
// 李縣令是一個(gè)清官,向上級(jí)報(bào)告說這里有難民要救濟(jì),請(qǐng)上面撥款,于是找了王太尉,并拿出了自己的3w兩白銀救助災(zāi)民,以私濟(jì)公
const Project1 = {
wood: '9w兩白銀',
rice: '19w兩白銀',
silks: '29w兩白銀'
}
// 王太尉作為一個(gè)貪官十分腐敗,皇帝問他的時(shí)候匯報(bào)如下
const Project2 = {
wood: '20w兩白銀',
rice: '40w兩白銀',
silks: '60w兩白銀'
}
// 我們?cè)趺磥韺?shí)現(xiàn)這種需求呢,通過上面的發(fā)布訂閱是做一個(gè)消息中間件是可以實(shí)現(xiàn)的,現(xiàn)在我們用代理試試
const needMoney = {
wood: 10,
rice: 20,
silks: 30,
}
const resultMoney = {
wood: 0,
rice: 0,
silks: 0,
}
//首先是李縣令
const LiJob = new Proxy(needMoney, {
get(target, key) {
console.log('王太尉問了一下李縣令' + key + '的款項(xiàng)情況')
return (target[key] - 1)
},
set(target, key, value) {
console.log('李縣令給災(zāi)民放款')
Reflect.set(target, key, value + 1)
resultMoney[key] = value + 1
}
})
// 然后是王太尉
const WangJob = new Proxy(LiJob, {
get(target, key) {
console.log('皇帝一下王太尉' + key + '的款項(xiàng)情況')
return (target[key] + 1) * 2
},
set(target, key, value) {
console.log('王太尉給李縣令撥款')
Reflect.set(target, key, (value/2) - 1)
}
})
// 最后是蒙在鼓里的皇帝
const Emperor = new Proxy(WangJob, {
get(target, key) {
console.log('你問了一下皇帝' + key + '的款項(xiàng)情況')
return target[key]
},
set(target, key, value) {
console.log(`皇帝撥款${key}為${value}`)
Reflect.set(target, key, value)
}
})

我們發(fā)現(xiàn),你眼見不一定為實(shí),知人知面不知心,這就是代理模式,每一層都不知道其它層干了什么,可以攔截?cái)?shù)據(jù)對(duì)其包裝后發(fā)布出去。
中介隔離作用:在某些情況下,一個(gè)客戶類不想或者不能直接引用一個(gè)委托對(duì)象,而代理類對(duì)象可以在客戶類和委托對(duì)象之間起到中介的作用,其特征是代理類和委托類實(shí)現(xiàn)相同的接口。
開閉原則,增加功能:代理類除了是客戶類和委托類的中介之外,我們還可以通過給代理類增加額外的功能來擴(kuò)展委托類的功能,這樣做我們只需要修改代理類而不需要再修改委托類,符合代碼設(shè)計(jì)的開閉原則。代理類主要負(fù)責(zé)為委托類預(yù)處理消息、過濾消息、把消息轉(zhuǎn)發(fā)給委托類,以及事后對(duì)返回結(jié)果的處理等。
在這個(gè)例子中
我們可以看到李縣令是清官,自己拿出3w讓上面少撥款。
王太尉是個(gè)貪官,貪了雙倍還多。
皇帝被蒙在鼓里聽信了王太尉。
然后皇帝開始放款,王太尉從中撈一筆,最后李縣令放款了,災(zāi)民收到了救濟(jì)款。
我們不難發(fā)現(xiàn),每一層都不知道別人到底干了什么,每一層都可以攔截?cái)?shù)據(jù),再包裝遞推給下一層,層層代理,每一層都不用關(guān)心躍層的事情,也做到了
松散耦合。
最后
看到這里,大家是不是已經(jīng)明白了觀察者模式和發(fā)布訂閱的區(qū)別和相同點(diǎn),并且也能知道為什么我們要在這里提到JS中的Proxy。我也終于可以去給小伙伴回去科普了??!
順便給大家留個(gè)作業(yè),看看Vue的雙向綁定機(jī)制是如何實(shí)現(xiàn)的。如果你仔細(xì)閱讀了這篇文章,閱讀源碼的時(shí)候一定會(huì)豁然開朗。