Typescript 實踐中的觀察者模式

前言

這一系列是對平時工做與學習中應用到的設計模式的梳理與總結。
因爲關於設計模式的定義以及相關介紹的文章已經不少,因此不會過多的涉及。該系列主要內容是來源於實際場景的示例。
定義描述主要來自 head first design patternUML 圖來源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

UML

在該類圖中,咱們看到四個角色:typescript

  • Subject: 目標
  • ConcreteSubject: 具體目標
  • Observer: 觀察者
  • ConcreteObserver: 具體觀察者

通常來講,目標自己具備數據,觀察者會觀察目標數據的變化,說是觀察者觀察,實際上是目標在變化時通知它的全部觀察者 「我變化了」。設計模式

實例

響應式對象

咱們想要構造一個對象,當這個對象的值改動時都將會通知。在javascript 中如何知道一個對象或者一個屬性是否更新了呢?咱們有幾個選項:函數

  • 一個顯式調用的 setState API
  • 使用 Object.defineProperty
  • 使用 Proxy

一個顯式調用的 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 函數看成 funccb 傳入,那就搭建起了數據層(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

總結

經過以上幾個例子,咱們能夠看出觀察者有一下幾個特色:

  • 鬆耦合,觀察者模式中 Observer 與 Subject 之間仍然存在抽象的耦合,可是發佈訂閱中因爲增長了中間層,因此二者完全消除了耦合。☑️
  • 很容易就能解決對象間的通訊問題。☑️
  • 過後沒有銷燬容易產生之外的結果。❌
相關文章
相關標籤/搜索