概述 觀察者模式又叫發佈 訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個目標對象(為了方便理解,以下將觀察者對象叫做訂閱者,將目標對象叫做發佈者)。發佈者的狀態發生變化時就會通知所有的訂閱者,使得它們能夠自動更新自己。 觀察者模式的使用場合就 ...
概述
觀察者模式又叫發佈 - 訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個目標對象(為了方便理解,以下將觀察者對象叫做訂閱者,將目標對象叫做發佈者)。發佈者的狀態發生變化時就會通知所有的訂閱者,使得它們能夠自動更新自己。
觀察者模式的使用場合就是:當一個對象的改變需要同時改變其它對象,並且它不知道具體有多少對象需要改變的時候,就應該考慮使用觀察者模式。
觀察者模式的中心思想就是促進鬆散耦合,一為時間上的解耦,二為對象之間的解耦。讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響到另一邊的變化。
實現
(function (window, undefined) {
var _subscribe = null,
_publish = null,
_unsubscribe = null,
_shift = Array.prototype.shift, // 刪除數組的第一個 元素,並返回這個元素
_unshift = Array.prototype.unshift, // 在數組的開頭添加一個或者多個元素,並返回數組新的length值
namespaceCache = {},
_create = null,
each = function (ary, fn) {
var ret = null;
for (var i = 0, len = ary.length; i < len; i++) {
var n = ary[i];
ret = fn.call(n, i, n);
}
return ret;
};
// 訂閱消息
_subscribe = function (key, fn, cache) {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
};
// 取消訂閱(取消全部或者指定消息)
_unsubscribe = function (key, cache, fn) {
if (cache[key]) {
if (fn) {
for (var i = cache[key].length; i >= 0; i--) {
if (cache[key][i] === fn) {
cache[key].splice(i, 1);
}
}
} else {
cache[key] = [];
}
}
};
// 發佈消息
_publish = function () {
var cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
ret = null,
stack = cache[key];
if (!stack || !stack.length) {
return;
}
return each(stack, function () {
return this.apply(_self, args);
});
};
// 創建命名空間
_create = function (namespace) {
var namespace = namespace || "default";
var cache = {},
offlineStack = {}, // 離線事件,用於先發佈後訂閱,只執行一次
ret = {
subscribe: function (key, fn, last) {
_subscribe(key, fn, cache);
if (!offlineStack[key]) {
offlineStack[key] = null;
return;
}
if (last === "last") { // 指定執行離線隊列的最後一個函數,執行完成之後刪除
offlineStack[key].length && offlineStack[key].pop()(); // [].pop => 刪除一個數組中的最後的一個元素,並且返回這個元素
} else {
each(offlineStack[key], function () {
this();
});
}
offlineStack[key] = null;
},
one: function (key, fn, last) {
_unsubscribe(key, cache);
this.subscribe(key, fn, last);
},
unsubscribe: function (key, fn) {
_unsubscribe(key, cache, fn);
},
publish: function () {
var fn = null,
args = null,
key = _shift.call(arguments),
_self = this;
_unshift.call(arguments, cache, key);
args = arguments;
fn = function () {
return _publish.apply(_self, args);
};
if (offlineStack && offlineStack[key] === undefined) {
offlineStack[key] = [];
return offlineStack[key].push(fn);
}
return fn();
}
};
return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
};
window.pubsub = {
create: _create, // 創建命名空間
one: function (key, fn, last) { // 訂閱消息,只能單一對象訂閱
var pubsub = this.create();
pubsub.one(key, fn, last);
},
subscribe: function (key, fn, last) { // 訂閱消息,可多對象同時訂閱
var pubsub = this.create();
pubsub.subscribe(key, fn, last);
},
unsubscribe: function (key, fn) { // 取消訂閱,(取消全部或指定消息)
var pubsub = this.create();
pubsub.unsubscribe(key, fn);
},
publish: function () { // 發佈消息
var pubsub = this.create();
pubsub.publish.apply(this, arguments);
}
};
})(window, undefined);
應用
假如我們正在開發一個商城網站,網站里有header頭部、nav導航、消息列表、購物車等模塊。這幾個模塊的渲染有一個共同的前提條件,就是必須先用ajax非同步請求獲取用戶的登錄信息。
至於ajax請求什麼時候能成功返回用戶信息,這點我們沒有辦法確定。更重要的一點是,我們不知道除了header頭部、nav導航、消息列表、購物車之外,將來還有哪些模塊需要使用這些用戶信息。如果它們和用戶信息模塊產生了強耦合,比如下麵這樣的形式:
login.succ(function (data) {
header.setAvatar(data.avatar); // 設置header模塊的頭像
nav.setAvatar(data.avatar); // 設置導航模塊的頭像
message.refresh(); // 刷新消息列表
cart.refresh(); // 刷新購物車列表
});
現在登錄模塊是由你負責編寫的,但我們還必須瞭解header模塊里設置頭像的方法叫setAvatar、購物車模塊里刷新的方法叫refresh,這種耦合性會使程式變得僵硬,header模塊不能隨意再改變setAvatar的方法名。這是針對具體實現編程的典型例子,針對具體實現編程是不被贊同的。
等到有一天,項目中又新增了一個收貨地址管理的模塊,這個模塊是由另一個同事所寫的,此時他就必須找到你,讓你登錄之後刷新一下收貨地址列表。於是你又翻開你3個月前寫的登錄模塊,在最後部分加上這行代碼:
login.succ(function (data) {
header.setAvatar(data.avatar);
nav.setAvatar(data.avatar);
message.refresh();
cart.refresh();
address.refresh(); // 增加這行代碼
});
我們就會越來越疲於應付這些突如其來的業務要求,不停地重構這些代碼。
用觀察者模式重寫之後,對用戶信息感興趣的業務模塊將自行訂閱登錄成功的消息事件。當登錄成功時,登錄模塊只需要發佈登錄成功的消息,而業務方接受到消息之後,就會開始進行各自的業務處理,登錄模塊並不關心業務方究竟要做什麼,也不想去瞭解它們的內部細節。改善後的代碼如下:
$.ajax('http:// xxx.com?login', function(data) { // 登錄成功
pubsub.publish('loginSucc', data); // 發佈登錄成功的消息
});
// 各模塊監聽登錄成功的消息:
var header = (function () { // header模塊
pubsub.subscribe('loginSucc', function(data) {
header.setAvatar(data.avatar);
});
return {
setAvatar: function(data){
console.log('設置header模塊的頭像');
}
};
})();
var nav = (function () { // nav模塊
pubsub.subscribe('loginSucc', function(data) {
nav.setAvatar(data.avatar);
});
return {
setAvatar: function(avatar) {
console.log('設置nav模塊的頭像');
}
};
})();
如上所述,我們隨時可以把setAvatar的方法名改成setTouxiang。如果有一天在登錄完成之後,又增加一個刷新收貨地址列表的行為,那麼只要在收貨地址模塊裡加上監聽消息的方法即可,而這可以讓開發該模塊的同事自己完成,你作為登錄模塊的開發者,永遠不用再關心這些行為了。代碼如下:
var address = (function () { // 地址模塊
pubsub.subscribe('loginSucc', function(obj) {
address.refresh(obj);
});
return {
refresh: function(avatar) {
console.log('刷新收貨地址列表');
}
};
})();
優缺點
優點
- 支持簡單的廣播通信,自動通知所有已經訂閱過的對象;
- 頁面載入後發佈者很容易與訂閱者存在一種動態關聯,增加了靈活性;
- 發佈者與訂閱者之間的抽象耦合關係能夠單獨擴展以及重用。
缺點
- 創建訂閱者本身要消耗一定的時間和記憶體,而且當你訂閱一個消息後,也許此消息最後都未發生,但這個訂閱者會始終存在於記憶體中;
- 雖然可以弱化對象之間的聯繫,但如果過度使用的話,對象和對象之間的必要聯繫也將被深埋在背後,會導致程式難以跟蹤維護和理解。
參考
- 《JavaScript設計模式與開發實踐》 第 8 章 發佈—訂閱模式
- 《JavaScript設計模式》 第 9 章 第 5 節 Observer(觀察者)模式
- http://www.cnblogs.com/TomXu/archive/2012/03/02/2355128.html