Typescript 实践中的观察者模式

前言

这一系列是对平时工作与学习中应用到的设计模式的梳理与总结。
由于关于设计模式的定义以及相关介绍的文章已经很多,所以不会过多的涉及。该系列主要内容是来源于实际场景的示例。
定义描述主要来自 head first design patternUML 图来源

定义

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

「观察者模式」定义了对象之间一对多的依赖关系,当一个对象状态改变时,它的所有依赖都会被通知并且自动更新。

结构

观察者模式的类图如下:

Typescript 实践中的观察者模式_第1张图片

在该类图中,我们看到四个角色:

  • Subject: 目标
  • ConcreteSubject: 具体目标
  • Observer: 观察者
  • ConcreteObserver: 具体观察者

一般来说,目标本身具有数据,观察者会观察目标数据的变化,说是观察者观察,其实是目标在变化时通知它的所有观察者 “我变化了”。

实例

响应式对象

我们想要构造一个对象,当这个对象的值改动时都将会通知。在javascript 中如何知道一个对象或者一个属性是否更新了呢?我们有几个选项:

  • 一个显式调用的 setState API
  • 使用 Object.defineProperty
  • 使用 Proxy

一个显式调用的 set API 基本上就是观察者模式的模版代码了,虽然它看起来很不智能(React:说我吗?),但实现成本确实很低。

class Subject {
    private state: T
    private observers: Observer[] = []

    constructor (state: T) {
        this.state = state
    }

    setState (state: Partial) {
        Object.assign(this.state, state)
        this.notify()
    }

    getState () {
        return this.state
    }

    attach (observer: Observer) {
        this.observers.push(observer)
    }

    notify () {
        this.observers.forEach(observer => observer.update(this))
    }
}

class Observer<
    T extends Subject,
    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(subject => { console.log('obA', subject.getState().a) })
const observerB = new Observer(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 稍微改造一下。

class Subject {
    state: T
    private observers: Observer[] = []

    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.observers.push(observer)
    }

    notify () {
        this.observers.forEach(observer => observer.update(this))
    }
}

class Observer<
    T extends Subject,
    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(subject => { console.log('obA', subject.state.a) })
const observerB = new Observer(subject => { console.log('obB', subject.state.a) })

subject.attach(observerA)
subject.attach(observerB)
subject.state.a = 10
// 输出 "obA 10"
// 输出 "obB 10"

看起来不错,我们已经完成了我们想要的,当然,这只是一个简单的例子,还不支持多层对象结构,不过这不是本文的重点。但是在某些情况下,我们只想监听 “相关” 的属性,这个需求需要如何实现呢?其实也很简单。

class Subject {
    state: T
    private observersMap: Map<
        keyof T,
        Set>
    > = 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) {
        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,
    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(
    (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(
    (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 = (subject: T) => {
    document.body.innerText = subject.state.a.toString()
}

const observerA = new Observer(
    (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 之间仍然存在抽象的耦合,但是发布订阅中由于增加了中间层,所以两者彻底消除了耦合。☑️
  • 很容易就能解决对象间的通信问题。☑️
  • 事后没有销毁容易产生以外的结果。❌

你可能感兴趣的:(javascript,设计模式)