發佈-訂閱模式是觀察者模式的一種,它定義一個對象和多個對象之間的依賴關係,當對象的狀態發生改變時,全部依賴它的對象都會收到通知。在JavaScript中,咱們通常使用事件模型來代替傳統的發佈-訂閱模式。javascript
在DOM編程中,咱們常常會監聽一些DOM事件,至關於訂閱這個事件,而後用戶觸發這個事件,咱們就能在回調中作一些操做。例如:java
document.body.addEventListener('click', function (e) {
console.log('clicked')
}, false)
document.body.click() // 模擬用戶點擊
複製代碼
咱們不知道何時用戶會觸發點擊事件,咱們只須要訂閱它,而後等到事件觸發,就能收到通知。ajax
簡單的版本編程
const event = {
clientList: [],
listen: function (fn) {
this.clientList.push(fn)
},
trigger: function () {
for (let i = 0, len = this.clientList.length; i < len; i++) {
const fn = this.clientList[i]
fn.apply(this, arguments)
}
}
}
複製代碼
可是這個簡單的版本有一些問題,若是訂閱者A和訂閱B它們對發佈者的感興趣的事件不同,可是不管發佈者觸發什麼事件,A和B都會收到通知。因此咱們添加eventChannel,對不一樣的訂閱者進行分類:api
const event = {
clientList: {},
listen: function (channel, fn) {
if (!this.clientList[channel]) {
this.clientList[channel] = []
}
this.clientList[channel].push(fn)
},
trigger: function () {
const channel = arguments[0]
const clientList = this.clientList[channel] || []
if (clientList.length === 0) {
return
}
for (let i = 0, len = clientList.length; i < len; i++) {
const fn = clientList[i]
fn.apply(this, arguments)
}
}
}
複製代碼
取消訂閱
若是某個訂閱者對以前訂閱的channel不感興趣了,還須要提供一個方法取消訂閱。app
event.remove = function (channel, fn) {
let clientList = this.clientList[channel] || []
if (clientList.length === 0) {
return
}
if (!fn) {
clientList = []
} else {
for (let i = 0, len = clientList.length; i < len; i++) {
const _fn = clientList[i]
if (_fn === fn) {
clientList.splice(i, 1)
}
}
}
}
複製代碼
假設咱們接到一個需求,用戶登陸以後,須要更新網站的header頭部、nav導航、購物車、消息列表等模塊的用戶信息。更新上面所列舉的模塊的前提條件就是經過ajax異步獲取用戶的登陸信息,由於ajax是異步的,何時返回登陸信息咱們是不知道的,最經常使用的作法是經過回調來解決,因而有以下代碼:異步
login.success(function(data) {
header.setAvatar(data.avatar)
nav.setAvatar(data.avatar)
message.refresh()
cart.refresh()
})
複製代碼
上面代碼的問題是,若是我負責的是登陸模塊,上面的其它header、nav、購物車模塊是其它同事負責的,我必須還得了解其它模塊的api,好比header模塊的setAvatar等等,這種耦合性使程序變得僵硬。若是哪天要重構其它模塊的代碼,那api的名字不能隨便修改,模塊名也不能隨意修改。若是項目又新增了一個地址模塊,這個模塊在用戶登陸以後也須要刷新,可是地址模塊是其它同事負責的,那這個同事還得找到你,叫你在登陸成功以後刷新地址列表。因而又增長了代碼:異步編程
login.success(function(data) {
header.setAvatar(data.avatar)
nav.setAvatar(data.avatar)
message.refresh()
cart.refresh()
address.refresh()
})
複製代碼
這種修改會讓人疲倦,讓開發人員失去耐心。這個時候,就須要發佈-訂閱模式出場,重構代碼。
使用發佈-訂閱模式重構的思路是,在用戶登陸成功以後,發佈一個登陸成功的消息,須要刷新用戶數據的模塊就能夠訂閱這個事件,而後去調用本身的方法更新數據或者作其它的業務處理,從而解耦了登陸模塊和其它模塊,登陸模塊不用關心其它模塊須要作什麼,也不用關心各模塊的內部細節。重構的代碼以下:網站
$.ajax('http://xxx.com?login', function (data) {
login.trigger('loginSuccess', data)
})
複製代碼
各模塊監聽登陸成功的消息:ui
const header = (function () {
login.listen('loginSuccess', function (data) {
header.setAvatar(data.avatar)
})
return {
setAvatar: function (data) {
console.log('設置header模塊的頭像')
}
}
})()
複製代碼
這樣就算其它模塊修改方法的名字或者哪一天又新增了模塊須要更新用戶數據,登陸模塊不須要關心,各個模塊本身處理就好了。
經過上面的應用咱們得出發佈-訂閱模式的優勢,一爲時間上的解耦,訂閱者不須要關心何時發佈者會發布事件;二爲對象之間的解耦,上面的登陸功能完美驗證了這個點。發佈-訂閱模式應用很是普遍,既能夠應用異步編程,也能夠幫助咱們完成更鬆耦合的代碼編寫。 發佈-訂閱模式也不是完美的,它也有本身的缺點。首先,建立訂閱者就要消耗時間和內存,當你訂閱一個消息,若是這個消息始終沒有發生,那這個訂閱者也會一直在內存中。它雖然弱化了對象之間的聯繫,但若是過分使用,對象和對象之間的必要聯繫也會深埋在背後,致使程序難以跟蹤維護和理解。特別是若是多個發佈者和訂閱者嵌套在一塊兒的時候,跟蹤bug也變得更加困難。