觀察者模式(Observer)
觀察者模式:定義了對象間一種一對多的依賴關係,當目標對象 Subject 的狀態發生改變時,所有依賴它的對象 Observer 都會得到通知。
簡單點:女神有男朋友了,朋友圈曬個圖,甜蜜宣言 “老孃成功脫單,希望你們歡喜”。各位潛藏備胎紛紛失戀,只能安慰自己你不是唯一一個。
模式特徵
- 一個目標者對象
Subject
,擁有方法:添加 / 刪除 / 通知Observer
; - 多個觀察者對象
Observer
,擁有方法:接收Subject
狀態變更通知並處理; - 目標對象
Subject
狀態變更時,通知所有Observer
。
Subject
添加一系列 Observer
, Subject
負責維護與這些 Observer
之間的聯繫,“你對我有興趣,我更新就會通知你”。
代碼實現
// 目標者類
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 observer of this.observers) {
observer.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();
// 輸出:
// 目標者通知我更新了,我是前端開發者
// 目標者通知我更新了,我是後端開發者
優勢
- 目標者與觀察者,功能耦合度降低,專注自身功能邏輯;
- 觀察者被動接收更新,時間上解耦,實時接收目標者更新狀態。
不完美
觀察者模式雖然實現了對象間依賴關係的低耦合,但卻不能對事件通知進行細分管控,如 “篩選通知”,“指定主題事件通知” 。
比如上面的例子,僅通知 “前端開發者” ?觀察者對象如何只接收自己需要的更新通知?上例中,兩個觀察者接收目標者狀態變更通知後,都執行了 update()
,並無區分。
“00後都在追求個性的時代,我能不能有點不一樣?”,這就引出我們的下一個模式。進階版的觀察者模式。“發佈訂閱模式”,部分文章對兩者是否一樣都存在爭議。
僅代表個人觀點:兩種模式很類似,但是還是略有不同,就是多了個第三者,因 JavaScript 非正規面嚮對象語言,且函數回調編程的特點,使得 “發佈訂閱模式” 在 JavaScript 中代碼實現可等同爲 “觀察模式”。
發佈訂閱模式(Publisher && Subscriber)
發佈訂閱模式:基於一個事件(主題)通道,希望接收通知的對象 Subscriber 通過自定義事件訂閱主題,被激活事件的對象 Publisher 通過發佈主題事件的方式通知各個訂閱該主題的 Subscriber 對象。
發佈訂閱模式與觀察者模式的不同,“第三者” (事件中心)出現。目標對象並不直接通知觀察者,而是通過事件中心來派發通知。
代碼實現
// 事件中心
let pubSub = {
list: [],
subscribe: function (key, fn) { // 訂閱
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
},
publish: function(key, ...arg) { // 發佈
for(let fn of this.list[key]) {
fn.call(this, ...arg);
}
},
unSubscribe: function (key) { // 取消訂閱
let fnList = this.list[key];
if (!fnList) return false;
if (!fn) {
// 不傳入指定取消的訂閱方法,則清空所有key下的訂閱
fnList && (fnList.length = 0);
} else {
fnList.forEach((item, index) => {
if (item === fn) {
fnList.splice(index, 1);
}
})
}
}
}
// 訂閱
pubSub.subscribe('onwork', time => {
console.log(`上班了:${time}`);
})
pubSub.subscribe('offwork', time => {
console.log(`下班了:${time}`);
})
pubSub.subscribe('launch', time => {
console.log(`吃飯了:${time}`);
})
// 發佈
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);
發佈訂閱的通知順序:
- 先訂閱後發佈時才通知(常規)
- 訂閱後可獲取過往以後的發佈通知 (QQ離線消息,上線後獲取之前的信息)
流行庫的應用
- jQuery 的
on
和trigger
,$.callback()
; - Vue 的雙向數據綁定;
- Vue 的父子組件通信
$on/$emit
jQuery 的 $.Callback()
jQuery 的 $.Callback() 更像是觀察者模式的應用,不能更細粒度管控。
function notifyHim(value) {
console.log('He say ' + value);
}
function notifyHer(value) {
console.log('She say ' + value);
}
$cb = $.Callbacks(); // 聲明一個回調容器:訂閱列表
$cb.add(notifyHim); // 向回調列表添加回調:訂閱
$cb.add(notifyHer); // 向回調列表添加回調:訂閱
$cb.fire('help'); // 調用所有回調: 發佈
Vue 的雙向數據綁定
利用 Object.defineProperty()
對數據進行劫持,設置一個監聽器 Observer
,用來監聽數據對象的屬性,如果屬性上發生變化了,交由 Dep
通知訂閱者 Watcher
去更新數據,最後指令解析器 Compile
解析對應的指令,進而會執行對應的更新函數,從而更新視圖,實現了雙向綁定。
-
Observer
(數據劫持) -
Dep
(發佈訂閱) -
Watcher
(數據監聽) -
Compile
(模版編譯)
關於 Vue 雙向數據綁定原理,可自行參考其它文章,或推薦本篇 《 vue雙向數據綁定原理》。
Vue 的父子組件通信
Vue
中,父組件通過 props
向子組件傳遞數據(自上而下的單向數據流)。父子組件之間的通信,通過自定義事件即 $on
, $emit
來實現(子組件 $emit
,父組件 $on
)。
原理其實就是 $emit
發佈更新通知,而 $on
訂閱接收通知。Vue
中還實現了 $once
(一次監聽),$off
(取消訂閱)。
// 訂閱
vm.$on('test', function (msg) {
console.log(msg)
})
// 發佈
vm.$emit('test', 'hi')
優勢
- 對象間功能解耦,弱化對象間的引用關係;
- 更細粒度地管控,分發指定訂閱主題通知
不完美
- 對間間解耦後,代碼閱讀不夠直觀,不易維護;
- 額外對象創建,消耗時間和內存(很多設計模式的通病)
觀察者模式 VS 發佈訂閱模式
類似點
都是定義一個一對多的依賴關係,有關狀態發生變更時執行相應的通知。
區別點
發佈訂閱模式更靈活,是進階版的觀察者模式,指定對應分發。
- 觀察者模式維護單一事件對應多個依賴該事件的對象關係;
- 發佈訂閱維護多個事件(主題)及依賴各事件(主題)的對象之間的關係;
- 觀察者模式是目標對象直接觸發通知(全部通知),觀察對象被迫接收通知。發佈訂閱模式多了箇中間層(事件中心),由其去管理通知廣播(只通知訂閱對應事件的對象);
- 觀察者模式對象間依賴關係較強,發佈訂閱模式中對象之間實現真正的解耦。
對象屬性數據攔截方式:
-
Object.defineProperty()
屬性描述符; - ES6 Class set ;
- ES6
Proxy
代理;
- *
參考文章:
本文首發Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。