概述
觀察者模式又叫釋出 – 訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者物件同時監聽某一個目標物件(為了方便理解,以下將觀察者物件叫做訂閱者,將目標物件叫做釋出者)。釋出者的狀態發生變化時就會通知所有的訂閱者,使得它們能夠自動更新自己。
觀察者模式的使用場合就是:當一個物件的改變需要同時改變其它物件,並且它不知道具體有多少物件需要改變的時候,就應該考慮使用觀察者模式。
觀察者模式的中心思想就是促進鬆散耦合,一為時間上的解耦,二為物件之間的解耦。讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響到另一邊的變化。
實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
(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導航、訊息列表、購物車之外,將來還有哪些模組需要使用這些使用者資訊。如果它們和使用者資訊模組產生了強耦合,比如下面這樣的形式:
1 2 3 4 5 6 |
login.succ(function (data) { header.setAvatar(data.avatar); // 設定header模組的頭像 nav.setAvatar(data.avatar); // 設定導航模組的頭像 message.refresh(); // 重新整理訊息列表 cart.refresh(); // 重新整理購物車列表 }); |
現在登入模組是由你負責編寫的,但我們還必須瞭解header模組裡設定頭像的方法叫setAvatar、購物車模組裡重新整理的方法叫refresh,這種耦合性會使程式變得僵硬,header模組不能隨意再改變setAvatar的方法名。這是針對具體實現程式設計的典型例子,針對具體實現程式設計是不被贊同的。
等到有一天,專案中又新增了一個收貨地址管理的模組,這個模組是由另一個同事所寫的,此時他就必須找到你,讓你登入之後重新整理一下收貨地址列表。於是你又翻開你3個月前寫的登入模組,在最後部分加上這行程式碼:
1 2 3 4 5 6 7 |
login.succ(function (data) { header.setAvatar(data.avatar); nav.setAvatar(data.avatar); message.refresh(); cart.refresh(); address.refresh(); // 增加這行程式碼 }); |
我們就會越來越疲於應付這些突如其來的業務要求,不停地重構這些程式碼。
用觀察者模式重寫之後,對使用者資訊感興趣的業務模組將自行訂閱登入成功的訊息事件。當登入成功時,登入模組只需要釋出登入成功的訊息,而業務方接受到訊息之後,就會開始進行各自的業務處理,登入模組並不關心業務方究竟要做什麼,也不想去了解它們的內部細節。改善後的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
$.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。如果有一天在登入完成之後,又增加一個重新整理收貨地址列表的行為,那麼只要在收貨地址模組里加上監聽訊息的方法即可,而這可以讓開發該模組的同事自己完成,你作為登入模組的開發者,永遠不用再關心這些行為了。程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
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