发布订阅模式

9,309 阅读7分钟

前言:之前对发布-订阅模式没有理解透,感觉跟观察者模式非常像,又看到有些文章说观察者模式就是发布-订阅模式,搞的有点头大。这篇文章以个人的理解对发布-订阅模式进行一次梳理,如果有错误或者不足的地方,希望大家不吝指出,共同进步!!!

为了更方便对布订阅模式的理解,本人又写了一篇由动图演示来解释的的文章,更加形象直白,其中对订阅发布的代码也做了修改,可以支持观察者模式:《这次,彻底搞懂发布订阅模式》,有兴趣的同学可以阅读。

一、从名字开始入手

名字提供了两个关键信息词:发布订阅,这是两个行为并分属于两个对象:发布者订阅者。可以用日常案例来解析这两种行为和对象,比如我们作为用户来订阅斗鱼的游戏直播,有订阅王者荣耀的有订阅英雄联盟的等等。

当各个游戏有新的比赛时斗鱼会通知对应的订阅观众,那这里我们用户就是订阅者,斗鱼是发布者。用户可以订阅自己感兴趣的游戏,斗鱼也可以发布不同的游戏消息,收到消息通知的用户可以自己选择是否观看直播或者取消订阅等行为。

所以从职责来上来讲,订阅者需要能够有订阅的功能(包含取消订阅),发布者需要有发布消息的功能。如果发布者发布的消息不是订阅者订阅的消息,那此订阅者不用关心,比如斗鱼通知有新的英雄联盟的赛事,那订阅王者荣耀的观众就收不到此消息。

当然一个订阅者可以同时订阅多个事件,比如既订阅英雄联盟也订阅王者荣耀,订阅者也同时可以对其他发布者进行订阅,比如我们还可以订阅掘金,有的用户喜欢前端知识,有的用户喜欢后端知识,也可以订阅微博等等,也就是说一个用户可以同时有很多的订阅事件。

二、建立一个订阅者

根据上面的分析,可以看出订阅者功能比较简单,只要有订阅和取消订阅的功能基本就可以了,先以订阅者入手构建一个订阅者的class:

//订阅者构造器
class Subscribe {
  constructor(name = 'subscriber') {
    this.name = name
    //随机id模拟唯一
    this.id = Date.now() + Math.ceil(Math.random() * 10000)
  }
  listen({
    publisher,//订阅的是哪个发布者
    message,//订阅的消息
    handler//收到消息后的处理方法
  }) {
    //订阅消息的回调函数
    if (publisher instanceof Publish) {
      //一个订阅者可以同时订阅多个发布者,所以回调函数要拼接上对应发布者的id
      this[message + '_' + publisher.id + "_handler"] = handler
      publisher.addListener(this, message)
    }
    return this
  }
  unlisten(publisher, message) {
    if (publisher instanceof Publish) {
      publisher.removeListener(this, message)
    }
    return this
  }
}

Subscribe代码比较简单有,两个方法listen和unlisten,分别用来订阅和取消订阅,订阅时要传入订阅的对象publisher,以及订阅消息message和收到消息通知后的处理函数handler,先不用关心publisher.addListener,在下面创建Publish的class时候再说明。

取消订阅时要传入所订阅的对象,以及订阅的消息,这里传入消息参数时,就只解除对此消息的订阅,如果不传消息参数就解除对这个订阅者所有消息的订阅,实现逻辑也放在了下面的Publish里面。

listen和unlisten都return this,这样一个订阅者实例就可以以链式的方式执行连续订阅或者取消订阅的方法,下面的发布者类里面的publish方法也一样可以链式发布消息。

三、建立一个发布者

先看代码:

//发布者构造器
class Publish {
  constructor(name = 'publisher') {
    this.messageMap = {} //消息事件订阅者集合对象
    //随机id模拟唯一
    this.id = Date.now() + Math.ceil(Math.random() * 10000)
    this.name = name
  }

  addListener(subscriber, message) { //添加消息订阅者
    if (!subscriber || !message) return false

    if (!this.messageMap[message]) { //如果消息列表不存在,就新建
      this.messageMap[message] = []
    }

    const existIndex = this.messageMap[message].findIndex(exitSubscriber => exitSubscriber.id === subscriber.id)

    if (existIndex === -1) {//不存在这个订阅者时添加
      this.messageMap[message].push(subscriber)
    } else {//存在这个订阅者时更新回调handler
      let handlerKey = message + "_" + this.id + "_handler";
      this.messageMap[message][existIndex] = subscriber;
    }
  }

  removeListener(subscriber, message) { //删除消息订阅者
    if (!subscriber) return false

    //如果传了message只删除此message下的订阅关系,否则删除此订阅者的所有订阅关系
    const messages = message ? [message] : Object.keys(this.messageMap)

    messages.forEach(_message => {
      const subscribers = this.messageMap[_message];

      if (!subscribers) return false;

      let i = subscribers.length;
      while (i--) {
        if (subscribers[i].id === subscriber.id) {
          subscribers.splice(i, 1)
        }
      }

      if (!subscribers.length) delete this.messageMap[_message]
    })
  }

  publish(message, info) { //发布通知
    const subscribers = this.messageMap[message] || []

    let handlerKey = message + "_" + this.id + "_handler";
    subscribers.forEach(subscriber => {
      subscriber[handlerKey](subscriber, info)
    })

    return this
  }
}

发布者主要功能也不复杂,就是在订阅者订阅消息的时候,执行addListener,把订阅者存储在自身的messageMap中,存储的规则是以订阅的消息作为key,存储结构关系如下:

messageMap = {
    message1:[subscriber1,subscriber2,subscriber3,...],
    message2:[subscriber1,subscriber2,subscriber3,...],
    message3:[subscriber1,subscriber2,subscriber3,...],
    ...
}

当发布者发布消息时,遍历对应的观察者列表,执行各自的回调handler,发布者在addListener添加订阅者的时候,有两个判断需要注意下:

1、如果一个消息是第一次被订阅,那就就以这个消息作为key建立一个订阅者列表;

2、如果一个订阅者多次订阅一个消息,那就更新他的回调函数,以最后一次的为最终回调。

发布者在removeListener时,如果只传入了一个订阅者,那就把这个订阅者在此发布者中所有消息的订阅关系全部删除,如果传入了确定的消息,那就只删除对应的消息下的订阅关系。当发布者某一条消息下的订阅者全部取消订阅时,就delete掉发布者的这个存储关系,减少空数组。

四、实例化测试案例

直接上代码:

//实例化发布者juejin和douyu
const juejin = new Publish('juejin')
const douyu = new Publish('douyu')

//实例化订阅者‘程序员A’和‘程序员B’
const programmerA = new Subscribe('programmerA')
const programmerB = new Subscribe('programmerB')

//订阅者订阅消息
//程序员A先订阅了juejin的Javascript,如果推送的是关于closure,就比较感兴趣。
//同时还订阅了juejin的Java,如果推送的内容价钱超过15,就买不起
programmerA.listen({
	publisher: juejin,
	message: 'JavaScript',
	handler: (self, info) => {
		let { title, duration, price } = info

		let result = `title[${title}]-> ${self.name} is not interested in it.`
		if(title === 'Closure') {
			result = `title[${title}]-> ${self.name} is interested in it.`
		}
		console.log(`receive the message JavaScript from ${juejin.name}:`, result)
	}
}).listen({
	publisher: juejin,
	message: 'Java',
	handler: (self, info) => {
		let { title, duration, price } = info
		let result = `price[${price}]: ${self.name} can not afford it.`
		if(price <= 15) {
			result = `price[${price}]: ${self.name} can afford it.`
		}
		console.log(`receive the message JavaScript from ${juejin.name}:`, result)
	}
})

//程序员B订阅了douyu的英雄联盟,表示很喜欢
//也订阅了douyu的王者荣耀,也是很喜欢
//同时还订阅了juejin的JavaScript,价钱小于10的,能支付的起
programmerB.listen({
	publisher: douyu,
	message: '英雄联盟',
	handler: (self, info) => {
		let { title } = info
		let result = `title[${title}]-> ${self.name} is interested in it.`
		console.log(`receive the message 英雄联盟 from ${douyu.name}:`, result)
	}
}).listen({
	publisher: juejin,
	message: '王者荣耀',
	handler: (self, info) => {
		let { title } = info
		let result = `title[${title}]-> ${self.name} is interested in it.`
		console.log(`receive the message 英雄联盟 from ${douyu.name}:`, result)
	}
}).listen({
	publisher: juejin,
	message: 'JavaScript',
	handler: (self, info) => {
		let { title, duration, price } = info
		let result = `price[${price}]: ${self.name} can not afford it.`
		if(price <= 10) {
			result = `price[${price}]: ${self.name} can afford it.`
		}
		console.log(`receive the message JavaScript from ${juejin.name}:`, result)
	}
})

//juejin发布消息通知
juejin.publish('JavaScript', {
	title: 'Prototype',
	duration: 20,
	price: 12
}).publish('JavaScript', {
	title: 'Closure',
	duration: 15,
	price: 8
}).publish('Java', {
	title: 'Interface',
	duration: 18,
	price: 10
})
//douyu发布消息通知
douyu.publish('英雄联盟', {
	title: 'RNG VS SSW',
	startTime: '2019-09-01 16:00',
}).publish('王者荣耀', {
	title: 'KPL联赛',
	startTime: '2019-08-30 20:30',
})

//程序员B取消对douyu的订阅,好好学习
programmerB.unlisten(douyu)

//发布者再次发布消息
juejin.publish('JavaScript', {
	title: 'React',
	duration: 20,
	price: 25
}).publish('JavaScript', {
	title: 'Vue',
	duration: 15,
	price: 20
})

douyu.publish('英雄联盟', {
	title: 'RNG VS SSW',
	startTime: '2019-09-02 16:00',
}).publish('王者荣耀', {
	title: 'KPL联赛',
	startTime: '2019-08-31 20:30',
})

五、总结

发布-订阅模式是基于消息联通的,必须在订阅方和发布方是同一个消息时才会有执行结果。订阅者只关注自己订阅的消息,每个订阅者同时可以订阅多个发布者对象。每个发布者在发布消息时不用关心此消息是否有订阅者,当发布者发布了被订阅者订阅的消息时,那么订阅者就根据消息详情做出对应的处理。

订阅者收到消息后具体怎么做已经跟发布者没有关联了,回调逻辑与发布逻辑完全解耦。订阅者也可以随意订阅自己感兴趣的对象和消息,发布-订阅模式在逻辑上的主谓关系比较明确。