likes
comments
collection
share

两个焦点:观察者模式和发布-订阅模式不一样?

作者站长头像
站长
· 阅读数 13

我们常常说:发布-订阅模式也叫做观察者模式,或者说观察者模式的别名就叫发布-订阅模式。实际上,是,也不是。《JavaScript设计模式与开发实践》一书中说分辨模式的关键是意图而不是结构。在意图方面上说,这两种模式的意图都是定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新;而从结构方面来说,观察者模式是观察者和被观察者(目标对象)之间的通讯,两者是直接关联的,而发布-订阅模式是发布者和订阅者之间的通讯,但是两者不是直接关联的。这么一所,肯定一头雾水,我们先看看javascript的24种设计模式,分三大类:

1、创建型模式:工厂方法模式、抽象工厂模式、单例模式、多例模式、建造者模式(生成器模式)、原型模式

2、结构型模式:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式

3、行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

一、观察者模式

两个焦点:观察者模式和发布-订阅模式不一样? 观察者模式就是观察者和被观察者之间的通讯。描述的是对象间的一种一对多的关系,即一个或多个观察者对目标对象的状态感兴趣,通过将自己本身依附在目标对象上以便关注所感兴趣的内容。目标对象的状态发生改变,若观察者对这些改变感兴趣,会发送一个通知消息,调用每个观察者的更新方法,当观察者不再对目标状态感兴趣时,他们可以简单将自己从中分离。举个例子:

class ObservedTarget {
    constructor() {
        this.observers = [] //用于存储所有的观察者
    }
    addObserver(...observer) {
        // console.log(...observer);
        this.observers.push(...observer) //添加观察者
    }
    notifyObserver(...args) {
        // 遍历观察者列表
        this.observers.forEach(item => {
            item.update(...args)
        })
    }
}
class Observer {
    constructor(name) {
        this.name = name
    }
    update(...args) {
        let content = [...args];
        console.log(`${this.name}接收到目标对象更新的状态是:${content}`);
    }
}
// 创建多个观察者
let observer1 = new Observer('observer1')
let observer2 = new Observer('observer2')
let observer3 = new Observer('observer3')
​
// 把观察者本身依附在目标对象上
let observerTarget = new ObservedTarget()
observerTarget.addObserver(observer1, observer2, observer3)//直接关联// 当目标对象更新内容时,通知所有的观察者
observerTarget.notifyObserver('这是我的新专辑!', '感谢粉丝对我的支持呀!')
​
//observer1接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!
//observer2接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!
//observer3接收到目标对象更新的状态是:这是我的新专辑!,感谢粉丝对我的支持呀!

以上不难看出,多个观察者将自己本身依附在目标对象上,这样就可以做到直接相关联,当目标对象的状态发生改变时,通知消息(广播出去),那么所有的观察者就都可以得到最新的消息了。对于学习vue的胞友来说,这个vue中双向绑定原理就是观察者模式的应用之一,可以看看双向绑定的原理:

let currentEffect = null;//在这里,通过这个全局变量将观察者和目标对象进行关联
class Dep {//就是目标对象
    constructor(val) {
        this._val = val
        this.effects = new Set() //存储依赖,依赖只收集一次
    }
    get value() { //get操作
        // 读取
        this.depend() //每次读取都会触发依赖收集
        return this._val
​
    }
    set value(newVal) { //set操作
        // 修改
        this._val = newVal
        this.notify() //值更新完毕后,通知更新
        return this._val
    }
    depend() { //收集依赖
        // 收集依赖时,需要先将收集的依赖存储起来,而且不重复收集依赖
        // 依赖是通过effectWatcher内部的回调函数配合effectWatcher实现的,所以需要关联到effectWatcher函数,可以先定义一个全局变量currentEffect
        if (currentEffect) {
            this.effects.add(currentEffect)
        }
    }
    notify() { //通知更新
        // 遍历所有依赖并执行
        this.effects.forEach(effect => {
            effect()
        })
    }
}
// effect函数
function effectWatcher(effect) {//就是个观察者
    currentEffect = effect //每收集一个依赖,都会关联到depend函数
    effect() //保证一上来就执行
    currentEffect = null
​
}
// 使用
const dep = new Dep('没有任何最新的动态~')
let content;
effectWatcher(() => {
    content = dep.value
    console.log(content);
})
// 当值发生改变
dep.value = '目标对象发布新专辑了!'
// 没有任何最新的动态~
// 目标对象发布新专辑了!

以上不难看出,Dep就充当目标对象,在vue中是依赖收集者;而就是effectWatcher观察者的身份了,在vue中是effect函数;其实这个vue的双向绑定原理的优化在于,我不再需要手动调用更新去广播,只要目标对象的值发生改变,会自动去广播。其实这就是观察者模式!

二、发布订阅模式

两个焦点:观察者模式和发布-订阅模式不一样? 发布-订阅模式也是通过⼀对⼀或者⼀对多的依赖关 系,当对象发⽣改变时,订阅⽅都会收到通知。在现实⽣活中类似场景是相当的多,⽐如公众号的订阅,预售消息订阅等等。

但是,相比于观察者模式,发布订阅模式多了个事件通道事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝订阅者和发布者的依赖关系,直白地说,订阅者只是订阅自己感兴趣的内容,并不关心目标对象的存在;而目标对象也只是发布自己想发布的内容,并不关心订阅者的具体存在。发布者和订阅者并无直接联系。

再回到观察者模式,它有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加(依附)到目标对象中进行管理;另一方面,目标在触发事件的时候,无法将通知操作委托给事件通道,而只能亲自去通知所有的观察者。

1、举个例子

实际上,在生活中处处都有发布-订阅的存在,只要我们曾经在DOM节点上面绑定过事 件函数,那我们就曾经使用过发布—订阅模式,只是你不知道而已,举个例子:

        <div id="box"></div>
        <script>
            function click() {
                console.log(3);
            }
            let box = document.getElementById('box')
            box.addEventListener('click', () => {
                console.log(1);
            })
            box.addEventListener('click', () => {
                console.log(2);
            })
            box.addEventListener('click', () => {
                click()
            })
box.click()//模拟用户点击
        </script>

当我点击box时,会同时打印1,2,3,如图所示:

两个焦点:观察者模式和发布-订阅模式不一样?

在这里需要监听用户的点击事件,但是谁知道用户啥时候点击呢?所以可以点阅box身上的click事件,当box节点被点击时,box就会向订阅者发布消息。

2、实现原理

其实,除去DOM事件,常用的还是自定义的其他事件,经过时间的洗礼,发布-订阅模式不再是简单的DOM事件可以体现的,现如今的发布-订阅模式除了发布、订阅,还有只订阅一次和取消订阅,在写发布-订阅模式之前呢,还需要知道一些关键的人物,下手才能行云流水,正所谓磨刀不误砍柴工:

  • 确认发布者的职责,发布者需要一个缓存列表,用于存放所有的订阅者回调函数,以便消息通知;在发布时,发布者会遍历该缓存列表,依次触发存放的订阅者回调函数。
  • 确认订阅者的需求,订阅者可以接收一些参数,这些参数是来自回调函数的,这些参数包括订阅者感兴趣内容相关的其他信息,当然,订阅者可以自行处理这些参数。
class PubSubTopics {//调度中心
    constructor() {
        this.subscribers = [] //用于存储所有的订阅者回调函数
    }
    subscrible(message, callback) { //订阅
        let callbacks = this.subscribers[message]
        if (!callbacks) { //不存在
            this.subscribers[message] = [callback]
        } else {
            callbacks.push(callback)
        }
    }
    publish(message, ...args) { //发布
        let callbacks = this.subscribers[message] || []
        callbacks.forEach(callback => callback(...args))
    }
    once(message, callback) { //只订阅一次
        let onceFn = (...args) => {
            callback.apply(this, args) //当场执行,不进入缓存列表
            this.remove(message) //再取消订阅
        }
        this.subscrible(message, onceFn)
    }
    remove(message, callback) { //取消订阅
        let callbacks = this.subscribers[message] || []
        if (!callback) { //没有传入具体的回调函数,则取消对应的所有订阅
            callbacks && (callbacks = [])
        } else {
            callbacks.forEach((cb, index) => {
                if (cb == callback) { //具名函数
                    callbacks.splice(index, 1)//删除
                }
            })
        }
    }
}
//测试
let subscriberA = new PubSubTopics()
subscriberA.subscrible('song', (song) => {
    console.log('new song=', song);
})
subscriberA.subscrible('teleplay', userA = (teleplay) => {
    console.log('new teleplay=', teleplay);
})
subscriberA.subscrible('teleplay', userB = (teleplay) => {
    console.log('new teleplay=', teleplay);
})
subscriberA.subscrible('teleplay', userC = (teleplay) => {
    console.log('new teleplay=', teleplay);
})
subscriberA.subscrible('movie', (movie) => {
    console.log('new movie=', movie);
})
// 取消订阅
subscriberA.remove('teleplay', userA)
subscriberA.remove('teleplay', userC)
// 只订阅一次
subscriberA.once('movie', (...args) => {
    console.log('我只订阅一次:', args);
})
subscriberA.publish('song', '李荣浩 乌梅子酱')
subscriberA.publish('teleplay', '张译 他是谁')
subscriberA.publish('movie', '易烊千玺 满江红')
​
//new song= 李荣浩 乌梅子酱
//new teleplay= 张译 他是谁
//new movie= 易烊千玺 满江红
//我只订阅一次: [ '易烊千玺 满江红' ]

至此,一个发布-订阅模式就大致实现了,其实在vue中,$on、$emit、$off、$once组合起来可不就是一个发布订阅模式了嘛。

三、小结

1、观察者模式

观察者模式包括两个主体:观察者和被观察者(目标对象),它属于行为型模式(关注对象之间的通信),也是一种将代码解耦的设计模式,观察者不需要直接调用被观察者的内部方法或属性获取通知。

观察者模式描述的是对象之间一种一对一或一对多的关系,观察者需要将自己注册(依附)在被观察上。

2、发布订阅模式

发布-订阅模式包括三个模块:发布者、订阅者和调度中心(办事大厅),发布者和订阅者两者之间是分离的,即订阅者只管在调度中心订阅,有人调用才响应;而发布者只管在调度中心广播。而且,在事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者。

发布-订阅模式的优点在于两个方面:一是时间上的解耦;而是对象之间的解耦。可以用在异步编程中,以便满足更加松耦合的需要。但是,万物都有两面性,发布订阅模式也不例外,该模式虽然可以弱化对象之间的耦合度,但是也不便于过渡使用,否则会难以跟踪和理解;另外,缓存大量的订阅者也需要消耗一定的时间和内存,一般是先订阅后发布,所以有些时候订阅者也许会永远存在内存中,但是这个消息可能始终未发生。 两个焦点:观察者模式和发布-订阅模式不一样?