事件是一種叫做觀察者的設計模式渐溶,這是一種創(chuàng)建松散耦合代碼的技術翔悠。對象可以發(fā)布事件,用來表示在該對象生命周期中有某個有趣的時刻到了元莫。然后其他對象可以觀察該對象,等待這些有趣的時刻到來蝶押,并通過運行代碼來響應踱蠢。
觀察者模式由兩類對象組成:主體和觀察者。主體負責發(fā)布事件棋电,同時觀察者通過訂閱這些事件來觀察該主體茎截。該模式的一個關鍵概念是主體不知道觀察者的任何事情,也就是說它可以獨自存在并正常運作即使觀察者不存在赶盔。從另一方面來說企锌,觀察者知道主體并能注冊時間的回調(diào)函數(shù)(事件處理程序)。
上面是紅寶書(《JavaScript高級程序設計(第三版)》)對自定義事件的描述于未。并且撕攒,它認為自定義事件就是觀察者模式陡鹃。觀察者模式有時候也叫發(fā)布/訂閱模式。
但實際上打却,觀察者模式和發(fā)布/訂閱模式有一點小小的不同杉适,即發(fā)布/訂閱模式有一個調(diào)度中心,對事件進行統(tǒng)一處理柳击。而觀察者模式會將處理事件的代碼分散在每個主體中猿推。發(fā)布/訂閱模式可以說是升級版觀察者模式。
紅寶書中實現(xiàn)自定義事件的代碼實際上是發(fā)布/訂閱模式捌肴。
什么時候用
個人認為蹬叭,觀察者模式主要解決的是模塊之間的通信問題。你有兩個類状知,當一個類中某個屬性發(fā)生變化的時候秽五,你要通知另一個類,讓它執(zhí)行對應的代碼饥悴,這個時候就可以使用觀察者模式坦喘。觀察者模式也叫消息機制。
原理
不管是觀察者模式還是發(fā)布/訂閱模式西设,原理都是一樣的瓣铣。
看到這個名字,我一開始以為贷揽,當消息產(chǎn)生時棠笑,主體將消息內(nèi)容通知給觀察者,觀察者執(zhí)行對應的函數(shù)禽绪。但實際上蓖救,實現(xiàn)的思路剛好相反。
執(zhí)行操作的是主體而不是觀察者印屁。觀察者起到的作用只是為消息注冊函數(shù)循捺,當主體發(fā)布消息時,會執(zhí)行注冊在該消息上的所有函數(shù)雄人。
回想一下瀏覽器中的事件(click巨柒,keydown),我們做的是給每個事件綁定響應的操作函數(shù)柠衍,我們寫的js屬于觀察者洋满。當事件發(fā)生時,瀏覽器就會執(zhí)行我們綁定的函數(shù)珍坊,其實原理是一樣的牺勾。
實現(xiàn)
發(fā)布/訂閱模式
var Observer = (function() {
// 消息容器,用來存放消息和要對應的操作函數(shù)
var __messages = {}
return {
// 注冊消息接口
regist: function(type, fn) {
// 當消息不存在時阵漏,創(chuàng)建一個該消息類型驻民,將回調(diào)函數(shù)推入執(zhí)行隊列中
if (typeof __messages[type] === 'undefined') {
__messages[type] = [fn]
} else {
// 消息類型存在時翻具,將回調(diào)函數(shù)推入執(zhí)行隊列中
__messages[type].push(fn)
}
},
// 發(fā)布消息接口
fire: function(type, args) {
// 當消息不存在時直接返回
if (!__messages[type])
return
args = args || {}
// 依次執(zhí)行消息對應的動作隊列
for (var i = 0, len = __messages[type].length; i < len; i++) {
__messages[type][i].call(this, args)
}
},
// 移除消息接口
remove: function(type, fn) {
if (__messages[type] instanceof Array) {
// 從后面開始遍歷,如果存在該動作則將其移除
for (var i = __messages[type].length - 1; i >= 0; i--) {
__messages[type][i] === fn && __messages[type].splice(i, 1)
}
}
}
}
})()
// 在觀察者中注冊消息
Observer.regist('test', function(args){
console.log(args.msg) // 消息內(nèi)容
})
// 主體發(fā)布消息
Observer.fire('test', { msg: '消息內(nèi)容' })
通過單例模式創(chuàng)建的Observer對象回还,就是發(fā)布/訂閱模式中的調(diào)度中心裆泳。私有變量__messages保存了消息類型及其對應的動作數(shù)組。觀察者通過Observer.regist接口給消息注冊動作柠硕,主體(發(fā)布者)通過Observer.fire發(fā)布消息工禾,執(zhí)行該消息的所有動作。通過Observer.remove方法可以移除對應動作蝗柔。
可以說闻葵,這個一個簡易的addEventListener的實現(xiàn)。
紅寶書中的實現(xiàn)與上述代碼的區(qū)別只是癣丧,它使用了構(gòu)造函數(shù)-原型混合模式創(chuàng)建Observer對象槽畔,可以實例化多個調(diào)度中心。
觀察者模式
// 觀察者列表
function ObserverList() {
this.observerList = []
if (typeof this.add !== 'function') {
ObserverList.prototype.add = function(obj) {
return this.observerList.push(obj)
}
ObserverList.prototype.count = function() {
return this.observerList.length
}
ObserverList.prototype.get = function(index) {
if (index > -1 && index < this.observerList.length) {
return this.observerList[index]
}
}
ObserverList.prototype.remove = function(observer) {
this.observerList.filter(function(item){
return item !== observer
})
}
}
}
// 主體(消息發(fā)布者)
function Subject() {
this.observers = new ObserverList()
if (typeof this.addObserver !== 'function') {
Subject.prototype.addObserver = function(observer) {
this.observers.add(observer)
}
Subject.prototype.removeObserver = function(observer) {
this.observers.remove(observer)
}
Subject.prototype.notify = function(args) {
var count = this.observers.count()
for (var i = 0; i < count; i++) {
this.observers.get(i).update(args)
}
}
}
}
// 觀察者
function Observer() {
this.update = function(){
// ...
}
}
主體內(nèi)部需要維護一個觀察者列表胁编,將需要觀察主體變化的對象加入這個列表厢钧。當消息發(fā)生時,可以調(diào)用notify方法通知所有觀察者(即調(diào)用觀察者的update方法)嬉橙。
可以看出早直,在每個主體中都要維護一個觀察者列表,造成代碼冗余憎夷。發(fā)布消息時,需要調(diào)用觀察者的方法昧旨,主體和觀察者耦合度高拾给。上面的代碼中,只有update一種消息類型兔沃,如果想要多種消息類型蒋得,主體需要定義多個函數(shù),十分麻煩乒疏。
因此额衙,發(fā)布/訂閱模式更加常用。