Typescript 实践中的观察者模式

本文转载于 SegmentFault 社区

社区专栏:实践是检验真理的唯一标准

作者:花生杀手



  

前言

 

这一系列是对平时工作与学习中应用到的设计模式的梳理与总结。


由于关于设计模式的定义以及相关介绍的文章已经很多,所以不会过多的涉及。该系列主要内容是来源于实际场景的示例。

定义描述主要来自 head first design pattern,UML 图来源:

https://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html


 

定义

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

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


  

结构

 

观察者模式的类图如下:

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

  • 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 稍微改造一下。

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"

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

 

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


总结



通过以上几个例子,我们可以看出观察者有一下几个特点:

·  松耦合,观察者模式中 Observer 与 Subject 之间仍然存在抽象的耦合,但是发布订阅中由于增加了中间层,所以两者彻底消除了耦合。☑️

·  很容易就能解决对象间的通信问题。☑️

·  事后没有销毁容易产生以外的结果。❌


 

点击左下角阅读原文,欢迎到 SegmentFault 思否社区 和文章作者展开更多互动和交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值