观察者模式和发布订阅模式是平常业务开发中最常见的设计模式,虽然网上大多数文章将二者归为一类,其实不然,它们两者之间还是有细微的差距。

先来个观察者模式的定义

  • 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的某个属性(或状态)发生变化时,会通知所有观察者对象,让它们自动更新

现实映射

  • 举一个🌰,高中的时候,我会经常去问老师问题,有时候遇到比较难的问题,老师一时半会解不开,老师会说**“你先去做其他的事情吧,一会儿我找到解题思路了来叫你”**。于是乎我先去做其他事情,等待老师的召唤。过了一会儿,老师叫另一个同学来叫我去办公室找他,于是我马上放下手中的活,冲向的老师的办公室……
  • 在这里例子里,我是一位观察者,而老师则是一位我观察的对象,当老师的状态发生了变化(指想出了题的思路),我就会接受到对应的信息,然后马上更新我自己的状态(指润去找老师)。

来点转换

  • 上述例子如果在发布-订阅模式里,我则摇身一变,变成了订阅者,专门订阅老师发布的通知信息,而老师则作为了发布者
  • 其实,上述例子还不能完全展示出定义所说的一对多关系,因为订阅者只有我一个人,但其实稍微扩展一下,变成多位同学向老师询问同一道题目,那这就是标准的观察者模式了,多位观察者“观察”老师的状态。

来点代码

  • 通过上述的定义和描述,大概可以知道,在观察者模式中,一共有两个类:发布者类和订阅者类。作为一个发布者,很容易可以想到它有下面几个基本方法:增加订阅者,通知订阅者,移除订阅者。思路有了,下面就直接实现。
// 发布者类
class Publisher {
    constructor() {
        this.observers = []      // Observer -> 观察员
    }

// 添加订阅者
    add(observers) {
        this.observers.push(...observers)
    }
// 移除订阅者
    remove(observer) {
        this.observers.forEach((item, index) => {
            if (item === observer) {
                this.observer.splice(index, 1)
            }
        })
    }
// 通知订阅者
    notify() {
        this.observers.forEach((item) => {
            item.update(); // 注意,订阅者的方法应该它们本身定义的
        })
    }
}
  • 发布者基本类设计完毕,下面开始设计下订阅者,其实订阅者很简单,它最核心的就一个方法:收到发布者的信息后,去进行状态更新。如下所示。
// 定义订阅者类
class Observer {
    constructor() {
        console.log('创建订阅者')
    }

    update() {
        console.log('更新状态')
    }
}
  • 好了,上述两段代码就是最基本的观察者模式实现,实际场景的观察者模式都是基于上面的代码进行迭代。比如下面我将会把之前说的实例进行代码实现
// 教师类
class Teachere extends Publisher {
    constructor() {
        super()
        // 
        this.answers = null
        this.observers = []
    }

    getAnswers() {
        return this.answers
    }

    setAnswers(ques) {
        this.answers = answers;
        console.log('老师想出了答案,快来办公室找老师')
        this.notify()
    }

    notify() {
        const that = this;
        this.observers.forEach(item => {
            item.update(that)
        })
    }
}

// 学生类
class Student extends Observer {
    constructor() {
        console.log("我是学生")
    }

    update(info) {
        const answer = info.getAnswers()
        // ... 进行相关的更新操作
    }
}

观察者模式和发布-订阅模式的区别

  • 其实在刚才的例子不算严格意义上的发布-订阅模式,因为发布-订阅模式中,发布者和订阅者二者之间是透明的,即它们是彼此感受不到对方的,无论是发布事件,还是订阅事件,都是交给一个统一的管理系统来进行处理的,这就类比于游戏原神中的每日委托一样,委托者在冒险家协会进行每日任务的委托,而旅行者-我则会去冒险家协会去领取每日任务,这个过程中,我们彼此感受不到对方,而是全权交给冒险家协会来进行管理的。
  • 如果放到Javascript世界中,无论是Vue的EventBus还是Nodejs中的EventEmitter类,它们的设计理念就是发布-订阅模式,即发布事件 - 订阅事件这一个过程。
// 订阅事件
event.on("callTeacher", function(...args){
  
})

// 发布事件
event.emit('callTeacher', arg1, arg2, arg3, ...args)
  • 很明显,和观察者模式不同的是,发布-订阅模式则将订阅者和发布者完全解耦,二者再也没有直接关联,它们的一切处理都统一交给第三方处理
  • 下面是一个简单的EventEmitter实现,为了加深对发布-订阅模式的认识。
// 发布-订阅模式的管理站
class EventEmitter {
    constructor() {
        // handlers -> map,存储事件和回调之间的关系
        this.handlers = {}
    }

    // eventname -> 事件名称 , cb -> 响应的回调函数
    on(eventName, cb) {
        if (!this.handlers[eventName]) {
            this.handlers[eventName] = []
        }

        this.handlers[eventName].push(cb);
    }

    emit(eventName, ...args) {
        if (this.handlers[eventName]) {
           // 这里用拷贝,否则once的删除会导致handlers的forEach遍历失序
            const handlers = this.handlers[eventName].slice()
            handlers[eventName].forEach(cb => {
                cb(...args)
            })
        }
    }

    off(eventName, cb) {
        if (this.handlers[eventName]) {
            this.handlers[eventName].forEach((item, index) => {
                if (item === cb) {
                    this.handlers[eventName].splice(index, 1)
                }
            })
        }
    }

    // 为事件注册单次监听器
    once(eventName, cb) {
        const wrapper = (...args) => {
            cb(...args)
            this.off(eventName, wrapper)
        }
        this.on(eventName, wrapper)
    }
}