发布订阅模式

前言:以前对发布-订阅模式没有理解透,感受跟观察者模式很是像,又看到有些文章说观察者模式就是发布-订阅模式,搞的有点头大。这篇文章以我的的理解对发布-订阅模式进行一次梳理,若是有错误或者不足的地方,但愿你们不吝指出,共同进步!!!javascript

1、从名字开始入手

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

当各个游戏有新的比赛时斗鱼会通知对应的订阅观众,那这里咱们用户就是订阅者,斗鱼是发布者。用户能够订阅本身感兴趣的游戏,斗鱼也能够发布不一样的游戏消息,收到消息通知的用户能够本身选择是否观看直播或者取消订阅等行为。java

因此从职责来上来说,订阅者须要可以有订阅的功能(包含取消订阅),发布者须要有发布消息的功能。若是发布者发布的消息不是订阅者订阅的消息,那此订阅者不用关心,好比斗鱼通知有新的英雄联盟的赛事,那订阅王者荣耀的观众就收不到此消息。程序员

固然一个订阅者能够同时订阅多个事件,好比既订阅英雄联盟也订阅王者荣耀,订阅者也同时能够对其余发布者进行订阅,好比咱们还能够订阅掘金,有的用户喜欢前端知识,有的用户喜欢后端知识,也能够订阅微博等等,也就是说一个用户能够同时有不少的订阅事件。后端

2、创建一个订阅者

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

//订阅者构造器
class Subscribe {
	constructor(name = 'subscriber') {
		this.name = name
		//随机id模拟惟一
		this.id = 'id-' + Date.now() + Math.ceil(Math.random() * 10000) 
	}
	listen({
		publisher,//订阅的是哪一个发布者
		message,//订阅的消息
		handler//收到消息后的处理方法
	}) {
	        //订阅消息的回调函数
		this[message + "_handler"] = handler
		publisher && publisher.addListener(this, message)
		return this
	}
	unlisten(publisher, message) {
		publisher && publisher.removeListener(this, message)
		return this
	}
}
复制代码

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

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

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

3、创建一个发布者

先看代码:测试

//发布者构造器
class Publish {
	constructor(name = 'publisher') {
		this.messageMap = {} //消息事件订阅者集合对象
		//随机id模拟惟一
		this.id = '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
		    this.messageMap[message][existIndex][message + "_handler"] = subscriber[message + "_handler"]
		}
	};

	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]

		if (!subscribers || !subscribers.length) return this
		
		subscribers.forEach(subscriber => subscriber[message + "_handler"](subscriber, ...info))

		return this
	};

};
复制代码

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

messageMap = {
    message1:[subscriber1,subscriber2,subscriber3,...],
    message2:[subscriber1,subscriber2,subscriber3,...],
    message3:[subscriber1,subscriber2,subscriber3,...],
    ...
}
复制代码

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

一、若是一个消息是第一次被订阅,那就就以这个消息做为key创建一个订阅者列表;

二、若是一个订阅者屡次订阅一个消息,那就更新他的回调函数,以最后一次的为最终回调。

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

4、实例化测试案例

直接上代码:

//实例化发布者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',
})
复制代码

5、总结

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

订阅者收到消息后具体怎么作已经跟发布者没有关联了,回调逻辑与发布逻辑彻底解耦。订阅者也能够随意订阅本身感兴趣的对象和消息,发布-订阅模式在逻辑上的主谓关系比较明确。

相关文章
相关标签/搜索