觀察者模式又叫發佈 - 訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個目標對象(爲了方便理解,如下將觀察者對象叫作訂閱者,將目標對象叫作發佈者)。發佈者的狀態發生變化時就會通知全部的訂閱者,使得它們可以自動更新本身。javascript
觀察者模式的使用場合就是:當一個對象的改變須要同時改變其它對象,而且它不知道具體有多少對象須要改變的時候,就應該考慮使用觀察者模式。html
觀察者模式的中心思想就是促進鬆散耦合,一爲時間上的解耦,二爲對象之間的解耦。讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響到另外一邊的變化。java
(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
至於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(); // 增長這行代碼 });
咱們就會愈來愈疲於應付這些突如其來的業務要求,不停地重構這些代碼。app
用觀察者模式重寫以後,對用戶信息感興趣的業務模塊將自行訂閱登陸成功的消息事件。當登陸成功時,登陸模塊只須要發佈登陸成功的消息,而業務方接受到消息以後,就會開始進行各自的業務處理,登陸模塊並不關心業務方究竟要作什麼,也不想去了解它們的內部細節。改善後的代碼以下:異步
$.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