说说你对观察者、发布订阅模式的理解

383 阅读12分钟

观察者模式是什么?

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新

观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯

观察者模式如何实现?

观察者模式一般至少有一个可被观察的对象 Subject ,可以有多个观察者去观察这个对象。二者的关系是通过被观察者主动建立的,被观察者至少要有三个方法(添加观察者、移除观察者、通知观察者)。

当被观察者将某个观察者添加到自己的观察者列表后,观察者与被观察者的关联就建立起来了。此后只要被观察者在某种时机触发通知观察者方法时,观察者即可接收到来自被观察者的消息。

// 被观察者对象
class Subject {

  constructor() {
    this.observerList = []; // 被观察者的列表
  }

  addObserver(observer)  //加入被观察者的列表
    this.observerList.push(observer);
  }

  removeObserver(observer) { // 移出被观察者的列表
    const index = this.observerList.findIndex(o => o.name === observer.name);
    this.observerList.splice(index, 1);
  }

  notifyObservers(message) { // 通知观察者
    const observers = this.observerList;
    observers.forEach(observer => observer.notified(message));
  }

}

// 观察者
class Observer {

  constructor(name, subject) {
    this.name = name;
    if (subject) {
      subject.addObserver(this);
    }
  }

  notified(message) { // 收到通知
    console.log(this.name, '收到消息', message);
  }

}

// 使用
const subject = new Subject();
const observerA = new Observer('观察者A', subject);// 观察者A主动申请加入被观察者的列表
const observerB = new Observer('观察者B', subject);// 被观察者主动将加入观察者B加入列表
subject.addObserver(new Observer('观察者C'));
subject.notifyObservers(`通知:${new Date()}开会`);

setTimeout(() => {
  subject.removeObserver(observerA);
  subject.removeObserver(observerC);
  subject.notifyObservers(`来办公室`);
  }, 1000)
/** 
观察者A 收到消息 '通知:Thu Apr 06 2023 09:13:35 GMT+0800 (中国标准时间)开会'
观察者B 收到消息 '通知:Thu Apr 06 2023 09:13:35 GMT+0800 (中国标准时间)开会'
观察者C 收到消息 '通知:Thu Apr 06 2023 09:13:35 GMT+0800 (中国标准时间)开会'
观察者C 收到消息 '来办公室'
**/ 

通过上面的代码分别实现了观察者和被观察者的逻辑,其中二者的关联有两种方式:

  • 观察者主动申请加入被观察者的列表

    会在观察者对象创建之初显式声明要被加入到被观察者的通知列表内

  • 被观察者主动将观察者加入列表

    在观察者创建实例后由被观察者主动将其添加进列表

观察者模式的应用场景?

在以下情况下,可以考虑使用观察者模式:

  • 当一个对象的状态改变需要通知其他多个对象时,可以使用观察者模式。例如,当一个数据模型的状态发生改变时,多个视图需要根据这个状态进行更新。
  • 当一个对象的行为依赖于其他多个对象时,可以使用观察者模式。例如,当一个控制器需要监控多个传感器的状态,根据这些状态来调整自身的行为。
  • 当一个对象需要在不同的场景下通知不同的观察者对象时,可以使用观察者模式。例如,当一个文本编辑器需要在保存文本、打印文本等不同场景下通知不同的观察者对象。

总的来说,观察者模式适用于需要实现发布/订阅机制的场景,其中一个对象的状态或行为改变需要通知其他多个对象,并且这些对象之间的关系比较固定

应用实例

DOM事件

我们曾经在DOM节点上面绑定过事件函数,那我们就使用过观察者模式,因为JS和DOM之间就是实现了一种观察者模式。

document.body.addEventListener("click", function() {
  alert("Hello World")
},false )
document.body.click() //模拟用户点击

以上js就是观察者,DOM就是被观察者,给DOM添加点击事件就相当于订阅了DOM,当DOM被点击,DOM就会通知js触发“ alert(“Hello World”) ”。

vue.js

  • 数据响应式系统

    Vue.js的数据响应式系统就是基于观察者模式实现的。当Vue实例的数据发生变化时,会自动触发对应的更新操作。

    因为你首先绑定了一个数据之后,浏览器并不知道你什么时候修改,你页面上所有绑定了该数据或者依赖该数据的节点其实就是一个预订列表,只有等你修改了该数据的值的时候,vue才会通知到依赖该数据的方法/数据进行相应的操作或刷新;

  • 计算属性

    Vue.js的计算属性也是基于观察者模式实现的。当计算属性所依赖的数据发生变化时,会自动重新计算该属性的值。

  • 监听器

    Vue.js的监听器也是基于观察者模式实现的。通过$watch()方法可以监听数据的变化,当数据发生变化时会自动触发回调函数。

  • 自定义事件

    在Vue.js中,组件之间的通信可以通过自定义事件来实现,也是基于观察者模式实现的。一个组件可以通过emit()方法发布事件,其他组件可以通过emit()方法发布事件,其他组件可以通过on()方法来订阅事件。

React.js

  • React Context API

    React Context API可以让我们在应用程序中共享数据,而不需要手动将数据通过props层层传递。我们可以使用Context API的Provider和Consumer组件来实现观察者模式。Provider组件提供了一个值,而Consumer组件可以订阅这个值的变化,从而实现观察者模式。

  • MobX

    MobX是另一个流行的状态管理库,它也基于观察者模式实现了一个全局的状态管理器。我们可以使用MobX中的observable()方法来创建可观察对象,使用autorun()方法来订阅可观察对象的变化,从而实现观察者模式。

观察者模式优缺点

优:

  • 松耦合:观察者模式可以使观察者和被观察者之间实现松耦合,使得它们之间的依赖关系降低,从而使系统更加灵活和易于维护。
  • 可扩展性:由于观察者模式中对象之间的关系比较松散,因此可以比较容易地添加新的观察者和被观察者,从而增加系统的可扩性。
  • 可重用性:观察者模式可以使观察者和被观察者之间的通信机制独立于具体的业务逻辑,从而可以提高代码的可重用性。

缺:

  • 如果被观察者有很多观察者,每个观察者都需要被通知,这可能会导致系统的性能下降。
  • 如果观察者之间有依赖关系,可能会导致系统复杂度增加。
  • 如果被观察者和观察者之间的通信机制不够清晰,可能会导致系统难以维护。

发布订阅模式是什么?

发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在

发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为调度中心|事件通道|发布订阅中心,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。

发布订阅模式如何实现?

与观察者模式相比,发布订阅核心基于一个中心来建立整个体系。其中发布者和订阅者不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息。

class PubSub { 	// 发布订阅中心
  constructor() {
    this.messages = {};
    this.listeners = {};
  }
  publish(type, content) {	// 发布者添加消息
    const existContent = this.messages[type];
    if (!existContent) {
      this.messages[type] = [];
    }
    this.messages[type].push(content);
  }
  subscribe(type, cb) {			// 添加订阅者
    const existListener = this.listeners[type];
    if (!existListener) {
      this.listeners[type] = [];
    }
    this.listeners[type].push(cb);
  }
  notify(type) { // 公布消息
    const messages = this.messages[type];
    const subscribers = this.listeners[type] || [];
    subscribers.forEach((cb, index) => cb(messages[index]));
  }
}

class Publisher { 	// 发布者
  constructor(name, context) {
    this.name = name;
    this.context = context;
  }
  publish(type, content) { // 发布消息
    this.context.publish(type, content);
  }
}

class Subscriber { 	//订阅者
  constructor(name, context) {
    this.name = name;
    this.context = context;
  }
  subscribe(type, cb) { //订阅消息
    this.context.subscribe(type, cb);
  }
}

// 假设有以下消息类型
  const TYPE_A = 'A';
  const TYPE_B = 'B';

// 创建发布订阅中心,为订阅者和发布者提供调度服务
const pubsub = new PubSub();

// 添加发布者
const publisherA = new Publisher('发布者A', pubsub);
const publisherB = new Publisher('发布者B', pubsub);

// 发布
publisherA.publish(TYPE_A, '十点开会'); 	// 发布者A添加了A型消息 (C只关注A型消息,不关心谁订阅了这个事件)
publisherA.publish(TYPE_B, '今晚加班');		// 发布者B添加了B型消息

// 添加订阅者
const subscriberA = new Subscriber('订阅者A', pubsub);
const subscriberB = new Subscriber('订阅者B', pubsub);
const subscriberC = new Subscriber('订阅者C', pubsub);

// 订阅
subscriberA.subscribe(TYPE_A, res => { // 订阅者A订阅了A型消息(订阅者A只关注A型消息本身,而不关心谁发布这个事件)
  console.log('订阅者A 收到', res)
});
subscriberB.subscribe(TYPE_A, res => { // 订阅者B订阅了A型消息
  console.log('订阅者B 收到', res)
});
subscriberC.subscribe(TYPE_B, res => { // 订阅者C订阅了B型消息
  console.log('订阅者C 收到', res)
});

// 发布订阅中心将消息公布出去
pubsub.notify(TYPE_A);
pubsub.notify(TYPE_B);

/**
订阅者A 收到 十点开会
订阅者B 收到 十点开会
订阅者C 收到 今晚加班
**/ 

以上发布订阅中心、发布者和订阅者三者有各自的实现,其中发布者和订阅者实现比较简单,只需完成各自发布、订阅的任务即可。其中订阅者可以在接收到消息后做后续处理。重点在于二者需要确保在与同一个发布订阅中心进行关联,否则两者之间的通信无从关联。

发布者的发布动作和订阅者的订阅动作相互独立,无需关注对方,消息派发由发布订阅中心负责。

发布订阅模式的应用场景?

在以下情况下,可以考虑使用发布订阅模式:

  • 当需要实现组件之间解耦和灵活的通信时,可以考虑使用发布订阅模式。
  • 当需要实现跨模块之间的通信时,可以考虑使用发布订阅模式。
  • 当需要处理异步操作的多个回调函数时,可以考虑使用发布订阅模式。
  • 当需要管理应用的状态时,可以考虑使用发布订阅模式。
  • 当需要为应用添加额外的功能时,可以考虑使用发布订阅模式。

总的来说,当需要实现事件驱动、解耦和灵活的通信、跨模块通信、状态管理、异步处理、插件开发等功能时,可以考虑使用发布订阅模式。使用发布订阅模式可以使代码更加灵活、可维护,同时也可以提高应用的性能和可扩展性。

应用实例

jQuery的事件机制

jQuery的事件机制就是基于发布订阅模式实现的。通过on()方法订阅事件,再通过trigger()方法发布事件。

vue.js

在Vue.js中,没有直接使用发布订阅模式,但是在某些场景下,可以使用Vue.js提供的一些API来实现类似于发布订阅模式的功能

  • Event Bus

    Vue.js提供了一个全局的事件总线(Event Bus)来实现组件之间的通信。我们可以使用emit()方法向事件总线发布事件,使用emit()方法向事件总线发布事件,使用on()方法来订阅事件。

  • Vuex

    Vuex是Vue.js官方提供的状态管理库,它基于发布订阅模式实现了一个全局的状态管理器。我们可以使用Vuex中的commit()方法来提交一个mutation,使用Vuex中的dispatch()方法来触发一个action,从而实现状态的修改和异步操作。

react.js

React.js并没有一个专门用于实现发布订阅模式的技术,但是它提供了一些可以用于实现发布订阅模式的第三方库和技术。其中,React Native Event Emitter是一个专门用于实现发布订阅模式的库,而React Redux和React Native Push Notification则是在实现其他功能的同时也可以用于实现发布订阅模式。

  • React Native Event Emitter

    React Native Event Emitter是一个事件管理库,它可以让我们在组件之间传递事件,从而实现发布订阅模式。我们可以使用Event Emitter中的addListener()方法来订阅一个事件,使用emit()方法来触发一个事件,从而实现组件之间的通信。

  • React Redux

    除了使用Redux中的subscribe()方法来实现观察者模式之外,我们还可以使用Redux中的dispatch()方法来触发一个事件,从而实现发布订阅模式。我们可以在Redux中定义一个事件类型,使用dispatch()方法来触发该事件,从而让订阅该事件的组件能够接收到通知。

  • React Native Push Notification

    React Native Push Notification是一个推送通知库,它可以让我们在应用程序中接收和处理推送通知。我们可以使用Push Notification中的addListener()方法来订阅一个事件,使用onNotification()方法来处理收到的推送通知,从而实现发布订阅模式。

订阅模式优缺点

优:

  • 松耦合:发布订阅模式可以实现松耦合,发布者和订阅者之间的依赖关系降低,从而使系统更加灵活和易于维护。
  • 可扩展性:由于发布订阅模式中的发布者和订阅者之间的关系比较松散,因此可以比较容易地添加新的发布者和订阅者,从而增加系统的可扩展性。
  • 灵活性:发布订阅模式可以支持多种消息类型和多个订阅者,从而提高了系统的灵活性。
  • 适应异步处理:由于发布订阅模式是异步的,因此可以适应异步处理的需要,提高系统的性能。

缺:

  • 系统复杂度增加:由于发布订阅模式中存在多个发布者和订阅者,以及消息队列等中间件,因此可能会增加系统的复杂度。
  • 可靠性问题:由于发布订阅模式是异步的,因此可能会存在消息丢失或者消息传递延迟等可靠性问题。
  • 调试困难:由于发布订阅模式中消息的传递是异步的,因此可能会增加调试的难度。

观察者和发布订阅区别

有人认为观察者模式和发布订阅模式是两种常用的设计模式。

也有人认为24种基本的设计模式中并没有发布订阅模式。观察者是经典软件设计模式中的一种,而发布订阅只是软件架构中的一种消息范式,观察者模式的变种而已。

但总的来说,观察者模式和发布订阅模式都可以用于实现对象之间的通信,但是它们的实现机制和应用场景有所不同,需要根据具体的业务需求选择合适的设计模式

观察者发布订阅
概念不同被观察者对象通知观察者对象状态的变化,观察者对象只能订阅并接收信息发布者对象将消息发布到调度中心,订阅者对象从调度中心订阅并接收信息。
组成不同2个角色(观察者,被观察者)3个角色(发布者、订阅者、发布订阅中心)
重点不同重点是被观察者重点是发布订阅中心
耦合度不同被观察者对象和观察者对象之间的耦合度较高,一旦被观察者对象发生变化,所有观察者对象都会收到通知发布者对象和订阅者对象之间的耦合度较低,订阅者对象只需要订阅自己感兴趣的消息。
事件驱动机制被观察者对象通常是直接调用观察者对象的方法来实现事件驱动机制调度中心通过事件驱动机制来分发消息
功能不同观察者模式通常只有一个观察者对象发布订阅模式可以有多个订阅者对象

总结

若你在代码中发现有watch、watcher、observe、observer、listen、listener、dispatch、trigger、emit、on、event、eventbus、EventEmitter这类单词出现的地方,很有可能是在使用观察者模式发布订阅的思想。等下次你发现有这些词的时候,不妨点进它的源码实现看看其他coder在实现观察者模式发布订阅时有哪些巧妙的细节。


最后一句

学习心得!若有不正,还望斧正。希望掘友们不要吝啬对我的建议。