[GoF] 觀察者模式 Observer
此篇為各筆記之整理,非原創內容,資料來源主要為:
- 《JavaScript 設計模式與開發實踐》
- Observer Pattern @ patterns.dev
TL;DR
觀察者模式,當一個物件的狀態發生改變時,所有依賴於它的物件都將得到通知。
概念
在沒有使用觀察者模式前,如果想要追蹤某個物件的狀態是否有發生變化時,必須不斷的去詢問與查詢該物件狀態;但透過觀察者模式,一旦主體(subject)發生改變(或其他合適的時機),它會主動通知所有訂 閱該主題的訂閱者。
舉例來說,如果有用過 react-hook-form 這個套件的話,如果想要知道表單最新的資料狀態時,可以主動使用 getValues('firstName')
來取出表單的值;但如果我們時希望表單一有更新就通知我們的話,則可以使用它所提供的 watch
方法,這個 watch('firstName')
的使用,就是觀察者模式,但 watch 的對象的資料狀態發生改變時,它就會主動通知元件,並得到最新的資料狀態。
名詞解釋
observable(又稱 subject)通常會具有下列功能和元素:
observer
:通常是可以被執行的「函式」,或者是物件中帶有可被執行的函式。當收到通知(notification)時,會觸發執行 observer。又常稱作subscriber
或listener
。subscribe()
:用來把 observer 加入 observers 陣列中的方法unsubscribe()
:用來把 observer 從 observers 陣列中移除的方法notify()
:一旦呼叫此方法,則所有該 observable 中的 observers 都會被執行。又常稱作dispatch()
或broadcast()
。
observable (subject) 可以想成是帶有上述這些元素的一個東東。
常見的觀察者模式
DOM 事件
實際上,很常使用到的 addEventListener
就類似 observer pattern:
btn.addEventListener('click', () => {
// ...
});
- onClick 中的 callback function 就類似 observer pattern 中的 observer
- addEventListener 就類似 observer pattern 中的
subscribe()
- btn 就像是 subject
Redux
Redux Store 最基本的程式邏輯也是利用 observer pattern 來通知所有訂閱到該資料的元件要 re-render。可以參考官方文件 Inside a Redux Store。
實作:自定義事件
在不考慮效能或其他功能的情況下,observer pattern 基本的實作並不複雜:
// https://www.patterns.dev/posts/observer-pattern/
class Observable {
constructor() {
// 把所有的 observers 都放在這個陣列中
this.observers = [];
}
// 透過 subscribe 把 callback function 保存在 observers 中
subscribe(func) {
this.observers.push(func);
// 需要的話也可以回傳一個 unsubscribe function
return () => {
const index = this.observers.indexOf(func);
this.observers.splice(index, 1);
};
}
// 或者另外提供 unsubscribe 的 function
unsubscribe(func) {
this.observers = this.observers.filter((observer) => observer !== func);
}
// notify 執行時,所有 observers 中 function 都會被執行
notify(data) {
this.observers.forEach((observer) => observer(data));
}
}
使用
function logger(data) {
console.log(`${Date.now()} ${data}`);
}
function greet(data) {
console.log(`Hi ${data}`);
}
// 建立一個 subject
const observable$ = new Observable();
// 透過 subscribe 把 fn 保存在 observers 中
const unsubscribeLogger = observable$.subscribe(logger);
const unsubscribeGreet = observable$.subscribe(greet);
// 呼叫該 subject 的 notify
observable$.notify('Aaron');
console.log('--- unsubscribe ---');
unsubscribeLogger();
observable$.notify('Aaron');
// 1648304648618 Aaron
// Hi Aaron
// --- unsubscribe ---
// Hi Aaron
實作:訂閱不同的主題
如果有需要的話,也 可以讓使用者訂閱及接受不同主題的內容,只需要將原本的 observers 由 observer[]
,變成一個 Map<string, observer[]>
的資料結構即可,實際的實作可以參考 observer pattern @ pjchender GitHub。
發佈/訂閱(Publish/Subscribe, pubsub)
PubSub 將觀察者模式做了泛型化,解決 Observer Pattern 中耦合和效能的問題。
PubSub 透過在 Publisher 和 Subscriber 中間添加了 Channel 層,來達到減少耦合。
參考資料
- Observer Pattern @ patterns.dev
- Observer Pattern by Ken @ React 前端讀書會
- 《JavaScript 設計模式與開發實踐》