对发布-订阅者模式的解析

1,541 阅读4分钟

介绍

在使用发布-订阅者模式之前,先了解什么是发布-订阅者模式。发布订阅者模式是一种一对多的依赖关系。多个对象(订阅者:subscriber)同时监听同一对象(发布者:publisher)的数据状态变化。当发布者的数据状态发生变化的时候,就会通知所有的订阅者,同时还可能以事件对象的形式传递一些消息。在生活中就有很多这样的例子,比如微信公众号的发布文章与用户的订阅公众号,报纸的发刊以及每家每户的订阅报纸等等,这些都是对发布订阅者模式很好的应用。

这样,我们就能知晓发布者以及订阅者包含的内容了,概括如下:

发布者(publisher):

  • 一个对象 subscribers:{type:[subscriberFn,...]} 包了含订阅的类型及其相关订阅者(由于订阅的事件可能多种所以此处需添加一个订阅类型)
  • 一个函数 subscribe(newSubscriberFn,type) 添加新的订阅者到对应订阅类型的订阅者数组中
  • 一个函数 unsubscribe(subscriberFn,type) 从数组中移除指定订阅者
  • 一个函数 publish(type) 当发布者的某一状态改变是,通知所有的订阅者,即遍历订阅者提供的方法

订阅者(subscriber):

  • 一个函数 subscriberFn() 当发布者状态发生改变时通知所使用的的回调函数

发布订阅的整个过程就如下所示:

理论的东西弄明白了,下面我们就进行具体的应用。

栗子

场景:某报社发布的报纸分为日报和月报。小王订阅了这家报社的日报,小李订阅了这家报纸的月报,小赵两类的报纸都订阅了。当报社有新的报纸出版了,小王、小李以及小赵都能阅读到他们订阅的报纸。

首先定义报社这个对象:

const PaperPublisher = {
	subscribers: {
		dailyPaper: [],
		monthlyPaper: [],
		all: []
	},
	subscribe (newSubscriberFn, type) {
		let type = type ? type : 'all';
		this.subscribers[type].push(newSubscriberFn);
	},
	unsubscribe (subscriberFn, type) {
		let type = type ? type : 'all',
			index = 0;
		for (let i = 0, len = this.subscribers[type].length; i < len; i++) {
			if (this.subscribers[type][i] == subscriberFn) {
				index = i;
				break;
			}
		}
		this.subscribers[type].splice(index,1);
	},
	publish (type,value) {
		for (let i = 0, len = this.subscribers[type].length; i < len; i++) {
			this.subscribers[type][i](value);
		}
		for (let i = 0, len = this.subscribers.all.length; i < len; i++) {
			this.subscribers.all[i](value);
		}
	},
	publishDailyPaper () {  // 发布日报
		this.publish('dailyPaper','日报的内容');
	},
	publishMonthlyPaper () {  // 发布月报
		this.publish('monthlyPaper','月报的内容');
	}
}

然后分别定义小王、小李、小赵

// 小王
const XiaoWangSubscriber = {
	subscriberFn (value) {
		console.log(`我是小王,正在读 ${value}`);
	}
}
// 小李
const XiaoLiSubscriber = {
	subscriberFn (value) {
        console.log(`我是小李,正在读 ${value}`);
	}
}
// 小赵
const XiaoZhaoSubscriber = {
	subscriberFn (value) {
		console.log(`我是小赵,正在读 ${value}`);
	}
}

接下来,让小王订阅日报,小李订阅月报,小赵两类的报纸都订阅

PaperPublisher.subscrib(XiaoWangSubscriber.subscriberFn,'dailyPaper');
PaperPublisher.subscrib(XiaoLiSubscriber.subscriberFn,'monthlyPaper');
PaperPublisher.subscrib(XiaoZhaoSubscriber.subscriberFn);

当报社发布日报,小王和小赵能收到报纸并阅读

PaperPublisher.publishDailyPaper();
 // 我是小王,正在读 日报的内容
 // 我是小赵,正在读 日报的内容

当报社发布月报,小李和小赵能收到报纸并阅读

PaperPublisher.publishMonthlyPaper();
 // 我是小李,正在读 月报的内容
 // 我是小赵,正在读 月报的内容

小王因为工作原因,去外地出差了,于是取消了日报的订阅

PaperPublisher.unsubscribe(XiaoWangSubscriber.subscriberFn,'dailyPaper');

以上就是整个栗子的全部代码了。

进一步提升

其实所有的发布者,前四点属性和方法都是必备的,所以我们能够提取出这些属性和方法存放在一个公用的对象中。在之后的使用中,我们就可以将它们复制到任何一个对象中,将这些对象转换为订阅者。特别的在函数unsubscribe和publish中都存在遍历subscribers中的数据的操作,所以在公用的对象中,定义了一个visitSubscribers()方法。下面就是对公用对象的定义:

var publisher = {
    subscribers: {
        all: []
    },
    subscribe: function (fn, type) {
        let type = type || 'all';
        if (typeof this.subscribers[type] === "undefined") {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn);
    },
    unsubscribe: function (fn, type) {
        this.visitSubscribers('unsubscribe', fn, type);
    },
    publish: function (type, publication) {
        this.visitSubscribers('publish', publication, type);
    },
    visitSubscribers: function (action, arg, type) {
        let pubtype = type || 'all',
            subscribers = this.subscribers[pubtype],
            i,
            max = subscribers.length;

        for (i = 0; i < max; i += 1) {
            if (action === 'publish') {
                subscribers[i](arg);
            } else {
                if (subscribers[i] === arg) {
                    subscribers.splice(i, 1);
                }
            }
        }
    }
};

除了公用的对象接着还需要定义一个函数来将一个普通的对象包装成一个发布者:

function makePublisher(o) {
    var i;
    for (i in publisher) {
        if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
            o[i] = publisher[i];
        }
    }
    o.subscribers = {any: []};
}

接下来,我们就可以以另外一种方式定义PaperPublisher对象了:

// PaperPublisher对象本身包括发布日报和发布周报的两个方法
var PaperPublisher = {
    publishDailyPaper () {  // 发布日报
		this.publish('dailyPaper','日报的内容');
	},
	publishMonthlyPaper () {  // 发布月报
		this.publish('monthlyPaper','月报的内容');
	}
};
// 将对象包装成发布者
makePublisher(PaperPublisher);

发布-订阅者模式的优缺点

优点:

  • 实现时间上的解耦(组件,模块之间的异步通讯)
  • 对象之间的解耦,交由发布订阅的对象管理对象之间的耦合关系.

缺点:

  • 创建订阅者本身会消耗内存,订阅消息后,也许,永远也不会有发布,而订阅者始终存在内存中.
  • 对象之间解耦的同时,他们的关系也会被深埋在代码背后,这会造成一定的维护成本.