這一系列是對平時工做與學習中應用到的設計模式的梳理與總結。
因爲關於設計模式的定義以及相關介紹的文章已經不少,因此不會過多的涉及。該系列主要內容是來源於實際場景的示例。
定義描述主要來自 head first design pattern,UML 圖來源。javascript
defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically. — head first design pattern
「觀察者模式」定義了對象之間一對多的依賴關係,當一個對象狀態改變時,它的全部依賴都會被通知而且自動更新。html
觀察者模式的類圖以下:java
在該類圖中,咱們看到四個角色:typescript
通常來講,目標自己具備數據,觀察者會觀察目標數據的變化,說是觀察者觀察,實際上是目標在變化時通知它的全部觀察者 「我變化了」。設計模式
咱們想要構造一個對象,當這個對象的值改動時都將會通知。在javascript 中如何知道一個對象或者一個屬性是否更新了呢?咱們有幾個選項:函數
一個顯式調用的 set API 基本上就是觀察者模式的模版代碼了,雖然它看起來很不智能(React:說我嗎?),但實現成本確實很低。學習
class Subject<T extends object> { private state: T private observers: Observer<this>[] = [] constructor (state: T) { this.state = state } setState (state: Partial<T>) { Object.assign(this.state, state) this.notify() } getState () { return this.state } attach (observer: Observer<this>) { this.observers.push(observer) } notify () { this.observers.forEach(observer => observer.update(this)) } } class Observer< T extends Subject<any>, K extends (subject: T) => unknown = (subject: T) => unknown, > { private cb: K constructor (cb: K) { this.cb = cb } update (subject: T) { this.cb(subject) } } const data = { a: 1, b: 2 } const subject = new Subject(data) const observerA = new Observer<typeof subject>(subject => { console.log('obA', subject.getState().a) }) const observerB = new Observer<typeof subject>(subject => { console.log('obB', subject.getState().a) }) subject.attach(observerA) subject.attach(observerB) subject.setState({ a: 10 }) // 輸出 "obA 10" // 輸出 "obB 10"
固然,光是這樣是不夠的,咱們後續還須要作 Diff 才能知道屬性值是否有變化,若是沒有變化的話就不須要 notify
,這裏就再也不贅述。setState
這種調用顯然沒有直接改屬性來的舒服,因此讓咱們用 Proxy 稍微改造一下。this
class Subject<T extends object> { state: T private observers: Observer<this>[] = [] constructor (state: T) { this.state = new Proxy(state, { get(target, key: keyof T) { return Reflect.get(target, key) }, set(target, key: keyof T, val) { Reflect.set(target, key, val) this.notify(key, val) // added return true } }) } attach (observer: Observer<this>) { this.observers.push(observer) } notify () { this.observers.forEach(observer => observer.update(this)) } } class Observer< T extends Subject<any>, K extends (subject: T) => unknown = (subject: T) => unknown, > { private cb: K constructor (cb: K) { this.cb = cb } update (subject: T) { this.cb(subject) } } const data = { a: 1, b: 2 } const subject = new Subject(data) const observerA = new Observer<typeof subject>(subject => { console.log('obA', subject.state.a) }) const observerB = new Observer<typeof subject>(subject => { console.log('obB', subject.state.a) }) subject.attach(observerA) subject.attach(observerB) subject.state.a = 10 // 輸出 "obA 10" // 輸出 "obB 10"
看起來不錯,咱們已經完成了咱們想要的,固然,這只是一個簡單的例子,還不支持多層對象結構,不過這不是本文的重點。可是在某些狀況下,咱們只想監聽 「相關」 的屬性,這個需求須要如何實現呢?其實也很簡單。spa
class Subject<T extends object> { state: T private observersMap: Map< keyof T, Set<Observer<any>> > = new Map() private keys: (keyof T)[] = [] constructor (state: T) { this.state = new Proxy(state, { get: (target, key: keyof T) => { this.keys.push(key) // added return Reflect.get(target, key) }, set: (target, key: keyof T, val) => { Reflect.set(target, key, val) this.notify(key, val) return true } }) } attach (observer: Observer<this>) { observer.run(this) this.keys.forEach((key) => { let observers = this.observersMap.get(key) if(!observers) { observers = new Set() this.observersMap.set(key, observers) } observers.add(observer) }) this.keys = [] } notify (key: keyof T, val: T[keyof T]) { const observers = this.observersMap.get(key) if(observers) { observers.forEach(observer => observer.update(val)) } } } class Observer< T extends Subject<any>, K extends (subject: T) => unknown = (subject: T) => unknown, F extends (val: T[keyof T]) => unknown = (val: T[keyof T]) => unknown > { private func: K private cb: F constructor (func: K, cb: F) { this.func = func this.cb = cb } run(subject: T) { this.func(subject) } update (val: T[keyof T]) { this.cb(val) } } const data = { a: 1, b: 2 } const subject = new Subject(data) const observerA = new Observer<typeof subject>( (subject) => { console.log('prop a should log when changed', subject.state.a) }, (val) => { console.log('a changed', val) } ) subject.attach(observerA) subject.state.a = 10 // 輸出 "a changed 10" subject.state.b = 10 // 沒有輸出 const observerB = new Observer<typeof subject>( (subject) => { console.log('prop a should log when changed', subject.state.b) }, (val) => { console.log('b changed', val) } ) subject.attach(observerB) subject.state.b = 100 // 輸出 "b changed 10"
通過改造事後,只有在 func
裏用到的屬性纔會響應修改了。若是咱們將一個 render
函數看成 func
和 cb
傳入,那就搭建起了數據層(Model)到視圖層(View)的橋樑,當數據變化時,那麼 DOM 就會響應變化而且更新。設計
const data = { a: 1, b: 2 } const subject = new Subject(data) const render = <T extends typeof subject>(subject: T) => { document.body.innerText = subject.state.a.toString() } const observerA = new Observer<typeof subject>( (subject) => { render(subject) }, () => { render(subject) } )
事實上,觀察者模式又叫發佈訂閱模式。可是在實踐中,它們對應着不一樣的設計,通常來講,發佈訂閱會在 Subject 與 Observer 之間增長一層中介來處理二者之間的耦合與溝通。不過本質上來講他倆沒有區別。
咱們經常用在組件間的通訊時的事件總線就是一個典型的發佈訂閱模式。
class EventBus { private events: { [key: string]: [Function]; } = {} on (eventName: string, cb: Function) { this.events[eventName] = this.events[eventName] || [] this.events[eventName].push(cb) } off (eventName: string, cb: Function) { const index = this.events[eventName].indexOf(cb) this.events[eventName].splice(index, 1) } emit (eventName: string, data?: unknown) { const cbs = this.events[eventName] if (cbs) { cbs.forEach(cb => cb(data)) } } } const eventBus = new EventBus() eventBus.on('testA', console.log) eventBus.on('testB', console.log) eventBus.emit('testA', 1) // 輸出 1
經過以上幾個例子,咱們能夠看出觀察者有一下幾個特色: