Javascript設計模式之我見:觀察者模式
大家好!本文介紹觀察者模式及其在Javascript中的應用。
模式介紹
定義
定義對象間一種一對多的依賴關系,使得每當一個對象改變狀態,則所有依賴于它的對象都會得到通知并被自動更新。
類圖及說明

Subject:主題\發布者
能夠動態地增加、取消觀察者。它負責管理觀察者并通知觀察者。
Observer:觀察者\訂閱者
觀察者收到消息后,即進行update操作,對接收到的信息進行處理。
ConcreteSubject:具體的主題\發布者
定義主題自己的業務邏輯,同時定義對哪些事件進行通知。
ConcreteObserver:具體的觀察者\訂閱者
每個觀察者在接受到消息后的處理反應時不同的,各個觀察者有自己的處理邏輯。
應用場景
- 當一個對象的改變需要同時改變其它對象,而不知道具體有多少對象有待改變。
-
當一個對象必須通知其它對象,而它又不能假定其它對象是誰。換言之, 你不希望這些對象是緊密耦合的。
-
對象僅需要將自己的更新通知給其他對象而不需要知道其他對象的細節。
優點
- 支持簡單的廣播通信,自動通知所有已經訂閱過的對象。
- 頁面載入后目標對象很容易與觀察者存在一種動態關聯,增加了靈活性。
- 目標對象與觀察者之間的抽象耦合關系能夠單獨擴展以及重用。
缺點
1、 松偶合導致代碼關系不明顯,有時可能難以理解。
2、 如果一個Subject被大量Observer訂閱的話,在廣播通知的時候可能會有效率問題。
觀察者模式在Javascript中的應用
類圖及說明

這里有兩個變化:
- ConcreteSubject與Subject的繼承關系改為委托關系。
- 刪除了Observer基類,直接將觀察者的update方法訂閱到Subject的events數組中。
應用場景
- 一個對象變化時觸發多個對象變化
- 一個對象調用其他對象的方法,而又不想與之耦合
示例
現在是找工作的季節,大家都在忙著找工作。大神可以單槍匹馬秒殺各種offer,高富帥也有各種關系幫忙找工作。
讓我們來看下高富帥是如何找工作的。
類圖

代碼
GaoFuShuai
function GaoFuShuai() { this._wang = new Wang(); } GaoFuShuai.prototype.findJob = function () { console.log("高富帥開始找工作了"); this._wang.help(); };
王哥
function Wang() { } Wang.prototype.help = function () { console.log("高富帥找工作啊,王哥來助你"); }
場景
function main() { var gaofushuai = new GaoFuShuai(); gaofushuai.findJob(); }
運行結果

分析
本設計有以下的缺點:
- 觀察者可能不止一個,如果增加李哥、張哥等觀察類,那就都要對應修改高富帥類,不符合開閉原則。
- 觀察者可能不僅僅要觀察高富帥的找工作情況,還要觀察高富帥的上學、娛樂等情況,這樣會有嚴重的耦合問題。
使用觀察者模式
現在使用觀察者模式來改進設計。
類圖

代碼
Subject
(function () { if (!Array.prototype.forEach) { Array.prototype.forEach = function (fn, thisObj) { var scope = thisObj || window; for (var i = 0, j = this.length; i < j; ++i) { fn.call(scope, this[i], i, this); } }; } if (!Array.prototype.filter) { Array.prototype.filter = function (fn, thisObj) { var scope = thisObj || window; var a = []; for (var i = 0, j = this.length; i < j; ++i) { if (!fn.call(scope, this[i], i, this)) { continue; } a.push(this[i]); } return a; }; } var Subject = function () { this._events = []; } Subject.prototype = (function () { return { //訂閱方法 subscribe: function (context, fn) { if (arguments.length == 2) { this._events.push({ context: arguments[0], fn: arguments[1] }); } else { this._events.push(arguments[0]); } }, //發布指定方法 publish: function (context, fn, args) { var args = Array.prototype.slice.call(arguments, 2); //獲得函數參數 var _context = null; var _fn = null; this._events.filter(function (el) { if (el.context) { _context = el.context; _fn = el.fn; } else { _context = context; _fn = el; } if (_fn === fn) { return _fn; } }).forEach(function (el) { //指定方法可能有多個 el.apply(_context, args); //執行每個指定的方法 }); }, unSubscribe: function (fn) { var _fn = null; this._events = this._events.filter(function (el) { if (el.fn) { _fn = el.fn; } else { _fn = el; } if (_fn !== fn) { return el; } }); }, //全部發布 publishAll: function (context, args) { var args = Array.prototype.slice.call(arguments, 1); //獲得函數參數 var _context = null; var _fn = null; this._events.forEach(function (el) { if (el.context) { _context = el.context; _fn = el.fn; } else { _context = context; _fn = el; } _fn.apply(_context, args); //執行每個指定的方法 }); }, dispose: function () { this._events = []; } } })(); window.Subject = Subject; })();
GaoFuShuai
function GaoFuShuai() { } GaoFuShuai.prototype.findJob = function () { console.log("高富帥開始找工作了"); window.subject.publishAll(null, "幫忙找工作"); }; GaoFuShuai.prototype.haveFun = function () { console.log("高富帥開始娛樂了"); window.subject.publishAll(null, "幫忙找樂子"); };
王哥
function Wang() { } Wang.prototype.help = function (actionStr) { console.log("王哥" + actionStr); };
李哥
function Li() { } Li.prototype.help = function (actionStr) { console.log("李哥" + actionStr); };
場景
function main() { var wang = new Wang(), li = new Li(), gaofushuai = new GaoFuShuai(); window.subject = new Subject(); window.subject.subscribe(wang.help); window.subject.subscribe(li.help); gaofushuai.findJob(); gaofushuai.haveFun(); }
運行結果

分析
這樣就符合開閉原則了,被觀察者與觀察者也不再直接耦合了。如果想繼續增加觀察者,則只需對應修改main即可,被觀察者GaoFuShuai類不用修改。
參考資料
浙公網安備 33010602011771號