零.序言
轉載&參考:
2.JavaScript 設計模式(六):觀察者模式與發布訂閱模式
一、觀察者模式(observer)
概要:
觀察者模式:定義了對象間一種一對多的依賴關系,當目標對象 Subject 的狀態發生改變時,所有依賴它的對象 Observer 都會得到通知。
這種模式的實質就是我們可以對某個對象的狀態進行觀察,並且在發生改變時得到通知(以進一步做出相應的行為)。
這種模式在平常日用中很常見,比如我們監聽 div 的 click 事件,其本質就是觀察者模式。 div.addEventListener('click', function(e) {...}) ,用文字描述:觀察 div 對象,當它被點擊了(發生變化),執行匿名函數(接受通知,然后做出相應行為)。
模式特征以及角色:
-
一個目標者對象
Subject
,擁有方法:添加 / 刪除 / 通知Observer
; -
多個觀察者對象
Observer
,擁有方法:接收Subject
狀態變更通知並處理; -
目標對象
Subject
狀態變更時,通知所有Observer
。
Subject
可以添加一系列 Observer
, Subject
負責維護與這些 Observer
之間的聯系,“你對我有興趣,我更新就會通知你”。
Subject - 被觀察者,發布者;
Observer - 觀察者,訂閱者;
示例:
為加深理解,以具體實例來看看:現有三個報社,報社一、二、三;有兩個訂報人,訂閱者1,訂閱者2。此處,報社就是被觀察者、訂閱人就是觀察者。
被觀察者: - 主要功能是維護訂閱自己的人以及分發消息
var Publish = function(name) { this.name = name; this.subscribers = []; // 數組中存放所有的訂閱者,本例中是所代表的觀察者的行為 } // 分發,發布消息 Publish.prototype.deliver = function (news) { var publish = this; // 各報社實例 // 通知所有的訂閱者 this.subscribers.forEach(item => { item(news, publish); // 每個訂閱者都收到了 news, 並且還知道是哪家報社發布的 }) return this; // 方便鏈式調用 }
觀察者: - 主要功能是(主動)訂閱或取消訂閱報社
// 訂閱 Function.prototype.subscribe = function(publish) { var sub = this; // 當前訂閱者這個人 // 1. publish.subscribers 中,名字可能重復 // 2. publish.subscribers 數組里面已有的人,不能再次訂閱 var alreadyExists = publish.subscribers.some(function(item) { return item === sub; }) // 如果出版社名單中沒有這個人,則加入進去 if (!alreadyExists) publish.subscribers.push(sub); return this; // 方便鏈式調用 } // 取消訂閱 Function.prototype.unsubscribe = function(publish) { var sub = this; // filter (過濾函數:循環便利數組的每一個元素,執行一個函數如果不匹配,則刪除該元素) publish.subscribers = publish.subscribers.filter(function(item){ return item !== sub ; }); return this; // 方便鏈式調用 }
以上所用的准備工作都已經做完了,接下來具體將具體的demo:
// 實例化發布者對象(報社) var pub1 = new Publish('報社一'); var pub2 = new Publish('報社二'); var pub3 = new Publish('報社三'); // 定義觀察者,當報社有了新的消息后,觀察者會收到通知 // 本例中以觀察者的行為代替觀察者對象,模擬 addEventListener var sub1 = function (news, pub) { console.log(arguments); document.getElementById('sub1').innerHTML += pub.name + news + '\n'; } var sub2 = function (news, pub) { console.log(arguments); document.getElementById('sub2').innerHTML += pub.name + news + '\n'; } // 執行訂閱方法,這一步是觀察者主動 sub1.subscribe(pub1).subscribe(pub2); sub2.subscribe(pub1).subscribe(pub2).subscribe(pub3); --------------------- 分割線 --------------------- var p1 = document.getElementById('pub1'); // dom var p2 = document.getElementById('pub2'); // dom var p3 = document.getElementById('pub3'); // dom // 事件綁定, 觸發 報社 的消息分發 p1.onclick = function() { pub1.deliver(document.getElementById('text1').value, pub1); } p2.onclick = function() { pub2.deliver(document.getElementById('text2').value, pub2); } p3.onclick = function() { pub3.deliver(document.getElementById('text3').value, pub3); }
其他資源部分:

1 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> 2 <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script> 3 <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> 4 5 <div class="col-lg-8"> 6 <div class="input-group"> 7 <span id="pub1" class="input-group-addon"> 8 報社一 9 </span> 10 <input v-model="user.name" type="text" class="form-control" id="text1"> 11 </div> 12 </div> 13 <div class="col-lg-8"> 14 <div class="input-group"> 15 <span id="pub2" class="input-group-addon"> 16 報社二 17 </span> 18 <input v-model="user.name" type="text" class="form-control" id="text2"> 19 </div> 20 </div> 21 <div class="col-lg-8"> 22 <div class="input-group"> 23 <span id="pub3" class="input-group-addon"> 24 報社三 25 </span> 26 <input v-model="user.name" type="text" class="form-control" id="text3"> 27 </div> 28 </div> 29 30 <div class="col-lg-8"> 31 訂閱者一 32 <textarea id="sub1" class="form-control" rows="5"></textarea> 33 訂閱者二: 34 <textarea id="sub2"class="form-control" rows="5"></textarea> 35 </div>
姿勢分解:
1.分割線之上是報社、訂閱者的實例化,以及訂閱者訂閱報社,如同我們在平常代碼中自定義 click 事件一樣;
2.分割線之下則是普通的添加 click 事件,這里面需要注意的是我們利用自己實現的 deliver 函數完成消息分發(通知)功能;
3.點擊“報社一”,兩個訂閱者都收到了通知,執行對應的行為,點擊”報社三“,因為只有訂閱者2 訂閱了這家報社,故只有訂閱者2 收到消息並完成行為;
另一個栗子:
// 目標者 class Subject { constructor() { this. observers = []; // 觀察者列表 } // 添加訂閱者 add(observer) { this.observers.push(observer); } // 刪除... remove(observer) { let idx = this.observers.findIndex(item => item === observer); idx > -1 && this.observers.splice(idx, 1); } // 通知 notify() { for(let o of this.observers) { o.update(); } } } // 觀察者 class Observer { constructor(name) { this.name = name; } // 目標對象更新時觸發的回調,即收到更新通知后的回調 update() { console.log(`目標者通知我更新了,我是:${this.name}`); } } // 實例化目標者 let subject = new Subject(); // 實例化兩個觀察者 let obs1 = new Observer('前端'); let obs2 = new Observer('后端'); // 向目標者添加觀察者 subject.add(obs1); subject.add(obs2); subject.notify();
優缺點:
優點明顯:降低耦合,兩者都專注於自身功能;
缺點也很明顯:所有觀察者都能收到通知,無法過濾篩選;
二、發布訂閱模式(Publisher && Subscriber)
概要:
發布訂閱模式:基於一個事件(主題)通道,希望接收通知的對象 Subscriber 通過自定義事件訂閱主題,被激活事件的對象 Publisher 通過發布主題事件的方式通知各個訂閱該主題的 Subscriber 對象。
發布訂閱模式與觀察者模式的不同,“第三者” (事件中心)出現。目標對象並不直接通知觀察者,而是通過事件中心來派發通知。
代碼實現
// 控制中心 let pubSub = { list: {}, // 訂閱 subscribe: function(key, fn) { if (!this.list[key]) this.list[key] = []; this.list[key].push(fn); }, //取消訂閱 unsubscribe: function(key, fn) { let fnList = this.list[key]; if (!fnList) return false; if (!fn) { // 不傳入指定的方法,清空所用 key 下的訂閱 fnList && (fnList.length = 0); } else { fnList.forEach((item, index) => { item === fn && fnList.splice(index, 1); }); } }, // 發布 publish: function(key, ...args) { for (let fn of this.list[key]) fn.call(this, ...args); } } // 訂閱 pubSub.subscribe('onwork', time => { console.log(`上班了:${time}`); }) pubSub.subscribe('offwork', time => { console.log(`下班了:${time}`); }) pubSub.subscribe('launch', time => { console.log(`吃飯了:${time}`); }) pubSub.subscribe('onwork', work => { console.log(`上班了:${work}`); }) // 發布 pubSub.publish('offwork', '18:00:00'); pubSub.publish('launch', '12:00:00'); // 取消訂閱 pubSub.unsubscribe('onwork');
其實,嚴格來講 DOM 的事件監聽是“發布訂閱模式”:
let loginBtn = document.getElementById('#loginBtn'); // 監聽回調函數(指定事件) function notifyClick() { console.log('我被點擊了'); } // 添加事件監聽 loginBtn.addEventListener('click', notifyClick); // 觸發點擊, 事件中心派發指定事件 loginBtn.click(); // 取消事件監聽 loginBtn.removeEventListener('click', notifyClick);
優缺點:
優點:解耦更好,細粒度更容易掌控;
缺點:不易閱讀,額外對象創建,消耗時間和內存(很多設計模式的通病)
三、兩種模式的關聯和區別
發布訂閱模式更靈活,是進階版的觀察者模式,指定對應分發。
-
觀察者模式維護單一事件對應多個依賴該事件的對象關系;
-
發布訂閱維護多個事件(主題)及依賴各事件(主題)的對象之間的關系;
-
觀察者模式是目標對象直接觸發通知(全部通知),觀察對象被迫接收通知。發布訂閱模式多了個中間層(事件中心),由其去管理通知廣播(只通知訂閱對應事件的對象);
-
觀察者模式對象間依賴關系較強,發布訂閱模式中對象之間實現真正的解耦。