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 之间仍然存在抽象的耦合,可是发布订阅中因为增长了中间层,因此二者完全消除了耦合。☑️
  • 很容易就能解决对象间的通讯问题。☑️
  • 过后没有销毁容易产生之外的结果。❌
相关文章
相关标签/搜索