##一.Observer(觀察者)模式
利用觀察者模式可以輕易地建立物件之間「一對多」的依賴關係; 利用觀察者模式的機制可以很容易的實現這種依賴關係的動態維護
(引自 黯羽輕揚:設計模式之觀察者模式(Observer Pattern) )
觀察者模式解耦了主題和觀察者,主題不清楚觀察者的細節,只知道所有觀察者都有 update 介面;觀察者對主題知道的更少,只能被動等待主題更新;各觀察者之間根本看不到彼此,更不知道沒有沒有關注同一主題了
松耦合意味著靈活性(彈性),只要保證實現既定介面,就可以獨立修改主題、觀察者,而不會影響二者的交互關係
##二.經典觀察者模式
觀察者模式概念比較簡單,經典實現需要增加一級抽象(Observ、Subject 的介面定義), JS 實現的簡單觀察者模式如下:
/* 經典觀察者模式
* Observer、Subject
* 也可以增加1級抽象,不過JS繼承太麻煩,此處從簡
* 手動維護List
*/
// Subject
function Subject(name) {
this.name = name;
this.list = [];
}
// 關注
Subject.prototype.regist = function(obj) {
this.list.push(obj);
};
// 取消關注
Subject.prototype.unregist = function(obj) {
var list = this.list;
for(var i = 0, len = list.length; i < len; i++) {
if (list[i] === obj) {
list.splice(i, 1);
}
}
};
// 通知所有粉絲
Subject.prototype.notify = function() {
var list = this.list;
for(var i = 0, len = list.length; i < len; i++) {
list[i].update({data: this.name});
}
};
// Observer
function Observer(id) {
this.id = id;
// 供Subject調用的更新方法
this.update = function(dataObj) {
// ...
console.log(this.id + ' received ' + dataObj.data + '\'s update');
};
}
// test
var subject = new Subject('My Topic');
var observer1 = new Observer(1);
var observer2 = new Observer(2);
var observer3 = new Observer(3);
subject.regist(observer1);
subject.regist(observer2);
subject.notify(); // 主題更新,通知所有觀察者
subject.regist(observer3); // 新加入observer3
subject.notify();
// observer1不玩了
subject.unregist(observer1);
subject.notify();
執行結果如下:
[caption id="attachment_663" align="alignnone" width="239"]
觀察者模式[/caption]
P.S. 如果對 1 級抽象感興趣,可以參考 黯羽輕揚:重新理解 JS 的 6 種繼承方式 自行實現 1 級抽象
##三.Publish/Subscribe(發布/訂閱)模式
觀察者模式分離了主題和觀察者,交互關係是嵌在二者內部的(必須實現既定介面),如果把交互關係再解耦出來的話,那就叫發布訂閱模式
發布訂閱模式在主題和觀察者之間新增了一層事件機制,由事件機制為雙方提供介面,維持交互關係,多這樣一層的好處是可以引入事件隊列、事件冒泡(不止 DOM 中可以有喔~)等等更多的控制
到這裡就已經差不多把解耦進行到底了,帶來巨大靈活性的同時,也存在一些不可避免的缺點,比如如果有一個觀察者崩潰了,因為解耦的關係,這個消息無法傳遞出去,主題不知道,周圍的觀察者也不知道,其他依賴於這個觀察者的模組也不會知道。(雖然理論上透過增強事件管理機制可以對這種情況加以控制,但這會使事件層變得越來越複雜,結構也越來越臃腫)
##四.自定義事件機制實現發布/訂閱模式
注意是「事件機制」,而不是「事件」,因為自定義事件是 DOM API,只能綁定在 DOM 元素上,具體請查看 Document.createEvent() - Web API Interfaces | MDN
所以必須自定義「事件機制」,實現的具體細節請查看 黯羽輕揚:JS 學習筆記 11_高級技巧 底部
DOM3 級 API 支援建立自定義事件,具體如下:
// 建立自定義事件物件
var event = document.createEvent('CustomEvent');
// 初始化事件
event.initCustomEvent('myeve', true, true);
// initCustomEvent 介面定義如下
/*
void initCustomEvent(
in DOMString type,
in boolean canBubble,
in boolean cancelable,
in any detail
);
*/
document.createEvent 和 initCustomEvent 都已經過時了,現在可以直接用構造函數建立自定義事件(例子請查看 JavaScript CustomEvent )物件。當然目前(2015-7-13)過時的東西相容性是最好的,詳情見 Document.createEvent() - Web API Interfaces | MDN
如果需要相容 [IE8-],就必須採取一些其他的手段,此處不展開,請查看 JavaScript 自定義事件
P.S. 如果專案引入了主流 JS 庫(框架),那麼一般都支援自定義事件,但也都是與 DOM 元素綁定的,非 DOM 元素物件的事件機制要自行實現,有一個極簡版的發布/訂閱實現,可以參考之 addyosmani/pubsubz ,程式碼很精巧,值得一看
##五.觀察者模式具體應用
觀察者模式很重要的一個應用場景是 Ajax 請求的回調邏輯,主題是 Ajax 請求傳回的數據,觀察者是回調函式中的各個邏輯塊(list 能夠保證執行順序),只需要在回調函式中 notify 所有觀察者即可,示例程式碼如下:
// 發布/訂閱模式的實現來自: https://github.com/addyosmani/pubsubz
(function(window) {
var topics = {},
subUid = -1,
pubsubz = {};
pubsubz.publish = function(topic, args) {
if (!topics[topic]) {
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func(topic, args);
}
return true;
};
pubsubz.subscribe = function(topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = (++subUid).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
pubsubz.unsubscribe = function(token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return false;
};
window.pubsubz = pubsubz;
}(this));
// 具體應用
var psz = pubsubz;
var eventName = 'DataUpdate';
var obs1 = psz.subscribe(eventName, function(data) {
console.log('obs1 received ' + data);
console.log('清除無效數據');
});
var obs2 = psz.subscribe(eventName, function(data) {
console.log('obs2 received ' + data);
console.log('顯示新數據');
});
var obs3 = psz.subscribe(eventName, function(data) {
console.log('obs3 received ' + data);
console.log('更新圖表列表');
});
// 模擬ajax
function ajax(url, callback) {
// 50ms後執行回調(假設已經拿到了數據)
setTimeout(function(data) {
data = 'data'; // 模擬數據
callback(data);
}, 50);
}
ajax('emptyUrl', function(data) {
psz.publish(eventName, data);
});
// 執行結果如下:
// obs3 received DataUpdate
// 更新圖表列表
// obs2 received DataUpdate
// 顯示新數據
// obs1 received DataUpdate
// 清除無效數據
// (逆序是因為pubsubz.publish採用了遞減迭代)
當然,這樣做也會帶來一定的問題,比如邏輯分散在各個觀察者上,會導致編碼難度增加(類似於事件驅動的缺點),使用時需要根據具體場景權衡
###參考資料
-
《JavaScript 設計模式》
-
相關博文若干
暫無評論,快來發表你的看法吧