1.JavaScript 設計模式系列 - 觀察者模式javascript
2.JavaScript 設計模式(六):觀察者模式與發佈訂閱模式css
觀察者模式:定義了對象間一種一對多的依賴關係,當目標對象 Subject 的狀態發生改變時,全部依賴它的對象 Observer 都會獲得通知。html
這種模式的實質就是咱們能夠對某個對象的狀態進行觀察,而且在發生改變時獲得通知(以進一步作出相應的行爲)。前端
這種模式在日常日用中很常見,好比咱們監聽 div 的 click 事件,其本質就是觀察者模式。 div.addEventListener('click', function(e) {...}) ,用文字描述:觀察 div 對象,當它被點擊了(發生變化),執行匿名函數(接受通知,而後作出相應行爲)。java
一個目標者對象 Subject
,擁有方法:添加 / 刪除 / 通知 Observer
;jquery
多個觀察者對象 Observer
,擁有方法:接收 Subject
狀態變動通知並處理;npm
目標對象 Subject
狀態變動時,通知全部 Observer
。bootstrap
Subject
能夠添加一系列 Observer
, Subject
負責維護與這些 Observer
之間的聯繫,「你對我有興趣,我更新就會通知你」。後端
Subject - 被觀察者,發佈者;設計模式
Observer - 觀察者,訂閱者;
爲加深理解,以具體實例來看看:現有三個報社,報社1、2、三;有兩個訂報人,訂閱者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();
優勢明顯:下降耦合,二者都專一於自身功能;
缺點也很明顯:全部觀察者都能收到通知,沒法過濾篩選;
發佈訂閱模式:基於一個事件(主題)通道,但願接收通知的對象 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);
優勢:解耦更好,細粒度更容易掌控;
缺點:不易閱讀,額外對象建立,消耗時間和內存(不少設計模式的通病)
發佈訂閱模式更靈活,是進階版的觀察者模式,指定對應分發。
觀察者模式維護單一事件對應多個依賴該事件的對象關係;
發佈訂閱維護多個事件(主題)及依賴各事件(主題)的對象之間的關係;
觀察者模式是目標對象直接觸發通知(所有通知),觀察對象被迫接收通知。發佈訂閱模式多了箇中間層(事件中心),由其去管理通知廣播(只通知訂閱對應事件的對象);
觀察者模式對象間依賴關係較強,發佈訂閱模式中對象之間實現真正的解耦。