需要了解的发布订阅模式和观察者模式 【复查篇】

470 阅读6分钟

前言

当我开始尝试去理解这两个概念..发现远远没那么简单,然后我又默默的再去理解了Vue的响应式,eventbus。。。后半部分偏向于场景原理实践例子。

有写的不清楚的地方,有些偏原理的文章,我也是去查阅的。有问题欢迎评论区建议。

好吧,正文开始,先放导图:

11632269024_.pic.jpg

发布订阅模式和观察者模式

这个两个模式,有时候会有些分不清楚,今天借这篇文章理一下相关的知识点。

主要是从理念出发,到使用,到实现。

1. 发布订阅模式

1.2 什么是发布订阅模式?

来自搜索引擎的解释:

发布订阅者模式就是一种一对多的依赖关系。多个订阅者(一般是注册的函数)同时监听同一个数据对象,当这个数据对象发生变化的时候会执行一个发布事件,通过这个发布事件会通知到所有的订阅者,使它们能够自己改变对数据对象依赖的部分状态。

  1. 多个订阅者
  2. 同一个数据对象
  3. 消息管理的工作,当这个数据发生改变,执行一个发布事件,通知所以订阅者

这样看来,一个完整的订阅发布模式,由发布者、订阅者、消息管理器三部分组成。

截屏2021-09-17 上午9.26.25.png

1.3 实现一个发布订阅模式?

class PubSub {
    constructor() {
        this.subscribers = {} // 消息对象
    }
    subscribe(type, fn) { // 订阅
        if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) {
          this.subscribers[type] = [];
        }
        
        this.subscribers[type].push(fn);
    }
    unsubscribe(type, fn) { // 取消订阅
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        this.subscribers[type] = listeners.filter(v => v !== fn);
    }
    publish(type, ...args) { // 发布
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        listeners.forEach(fn => fn(...args));        
    }
}


let ob = new PubSub();
ob.subscribe('add', (val) => console.log(val)); // 给type为‘add’的属性上加一个发布函数 
ob.publish('add', 1); // 当数据改变了,通知订阅,修改数据
  1. subscribe 函数,接收type,和一个fn

看看this.subscribers = {}这个消息对象里面有没有type这个属性,如果没有就添加这个type,设置this.subscribers[type] = [];

否则,this.subscribers[type].push(fn);,把订阅函数push进去

  • 这里我们使用了Object.prototype.hasOwnProperty.call(),去判定属性是否是来自原型链。
  • 使用原型链上真正的 hasOwnProperty 方法,因为javascript没有将hasOwnProperty作为一个敏感词,所以我们很有可能将对象的一个属性命名为hasOwnProperty
  • 这样一来就无法再使用对象原型的 hasOwnProperty 方法来判断属性是否是来自原型链。

(像下面的这个例子👇🏻)

var jing = {
   hasOwnProperty: function() {
       return false;
   },
   bar: 'Here be dragons'
};

console.log(jing.hasOwnProperty('bar'));  // false


var jing = {
   hasOwnPropertyTrue: function() {
       return false;
   },
   bar: 'Here be dragons'
};

console.log(jing.hasOwnProperty('bar'));  // true
  1. unsubscribe 取消订阅
unsubscribe(type, fn) { // 取消订阅
    let listeners = this.subscribers[type]; 
    if (!listeners || !listeners.length) return; 
    // 先要确定这个需要取消的订阅type是否存在,如果不存在或者type里面没订阅函数,直接返回
    this.subscribers[type] = listeners.filter(v => v !== fn); 
    // 过滤掉对应type里面的某个需要取消订阅的函数
}
  1. publish 发布 某个type里面的订阅函数执行
publish(type, ...args) { // 发布
    let listeners = this.subscribers[type];
    if (!listeners || !listeners.length) return; 
    // 先要确定这个需要取消的订阅type是否存在,如果不存在或者type里面没订阅函数,直接返回
    listeners.forEach(fn => fn(...args)); 
    // 执行这个type的订阅函数(和参数一起)      
}

2. 观察者模式

2.1 什么是观察者模式

观察者模式,比发布订阅模式少了事件管理中心这个节点,目标和观察者可以直接进行交互,而发布订阅模式,由调度中心进行处理,订阅者和发布者互不干扰

  1. 目标
  2. 观察者

截屏2021-09-17 上午7.58.41.png

2.2 实现一个观察者模式?


class Subject {
    constructor(name) {
        this.name = name;
        this.observers = [];
        this.state = '状态1';
    }

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

    changeState(newstate) {
        this.state = newstate;
        this.observers.forEach(o => o.update(newstate))
    }
}



class Observer {
    constructor(name) {
        this.name = name;
    }
    update(newstate) {
        console.log(this.name, newstate)
    }
}

let sub1 = new Subject('被观察者');
let obj1 = new Observer('观察者1');
let obj2 = new Observer('观察者2');

sub1.attach(obj1);
sub1.attach(obj2);
console.log(sub1.changeState('状态2'))

// 观察者1 状态2
// 观察者2 状态2

观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,会造成代码的冗余。

而发布订阅模式则统一由调度中心处理,消除了发布者和订阅者之间的依赖。

3. 场景复现

3.1 javascript 原生事件的观察者模式

ele.addEventListener('click', () => {});

addEventListener就相当于注册了一个观察者,当观察到‘click’事件的时候,作出一些处理。

3.2 vue 中的发布订阅使用 eventBus

截屏2021-09-18 上午8.24.05.png

(暂时不考虑vuex之类的状态管理器传递数据)

当我们希望5的数据能传到6的时候,我们需要走a-b-c-d的路线,这个组件传值的方法有些麻烦。

我们可以使用vue中的on,on,off,$emit,实现发布订阅模式

创建一个事件中心绑定到每个组件上面,只有一个组件有新的数据发布到上面,其他已经订阅的组件就可以同步获取

截屏2021-09-18 上午8.30.13.png

组件的状态被放在事件中心里面,当状态发生改变了,事件中心会通知那些订阅了状态的组件进行状态更新。

创建全局EventBus

var EventBus = new Vue();

Object.defineProperties(Vue.prototype, {
  $bus: {
    get: function () {
      return EventBus
    }
  }
})

使用:

this.$bus.$emit() // 发布
this.$bus.$on() // 订阅
this.$bus.$off() // 取消订阅

EventBus 可以较好的实现兄弟组件之间的数据通讯。

3.3 vue 中的数据劫持+发布订阅+观察者模式

截屏2021-09-18 上午9.03.56.png

在谈到vue的响应式原理的时候,我们会说到vueJS采用数据劫持结合发布订阅模式。

  • 数据劫持: Object.defineProperty()来劫持data中各个属性的setter、getter (setter和getter是对象的存储器属性,是一个函数,用来获取和设置值)
  • 发布订阅模式: 在数据变动时,发布消息给订阅者,触发响应的监听回调

vue中dep和watcher示例,watcher订阅dep,dep通知watcher执行update

  • dep.js:
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {        //添加订阅者
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {      //删除订阅者
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {               //通知订阅者
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()         //订阅者update方法
    }
  }
}
  • watcher.js中的update方法
  update () {                                 
    //watcher作为订阅者的update方法
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

3.4 nodejs

Node 自身提供 events 模块,是发布/订阅模式的一个简单实现,Node 中部分模块都继承自它。

实例

var events = require('events');
var emitter = new events.EventEmitter();
 
emitter.on("event-titus", function (message) {
	console.log(message);
})
 
emitter.emit("event-titus", "I am titus");

// off once

EventEmitter

  • on 绑定 订阅
  • emit 发布
  • off 取消订阅
  • once 只在发布的时候执行一次

4. 总结对比

发布订阅模式属于广义上的观察者模式,发布订阅模式是最常用的一种观察者模式的实现,并且从解耦和重用角度来看,更优于典型的观察者模式

在来看看两种模式的对比:

  • 发布订阅:通由调度中心进行处理,订阅者和发布者互不干扰
  • 观察者: 目标和观察者可以直接进行交互

有写的不当的地方欢迎评论区指正❤

文章就到这里了

我是婧大

一个在默默准备秋招的大四学仔。

希望可以和你一起学习一起进步呀

wx: lj18379991972

欢迎👏🏻加好友,一起学前端。❤