零.序言
轉載&參考:
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);
優缺點:
優點:解耦更好,細粒度更容易掌控;
缺點:不易閱讀,額外對象創建,消耗時間和內存(很多設計模式的通病)
三、兩種模式的關聯和區別
發布訂閱模式更靈活,是進階版的觀察者模式,指定對應分發。
-
觀察者模式維護單一事件對應多個依賴該事件的對象關系;
-
發布訂閱維護多個事件(主題)及依賴各事件(主題)的對象之間的關系;
-
觀察者模式是目標對象直接觸發通知(全部通知),觀察對象被迫接收通知。發布訂閱模式多了個中間層(事件中心),由其去管理通知廣播(只通知訂閱對應事件的對象);
-
觀察者模式對象間依賴關系較強,發布訂閱模式中對象之間實現真正的解耦。
