发布订阅模式
发布/订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型来替代传统的发布/订阅模式。
定义
发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
使用发布订阅模式的好处:
- 支持简单的广播通信,自动通知所有已经订阅过的对象。
- 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
- 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。
发布-订阅的实现
var event = {
cache : [], //存放订阅消息
pub : function(){ //发布消息
for(var i= 0;fn;fn = this.cache[i++]){
fn.call(this.arguments)
}
},
sub : function(fn){ //增加订阅者
this.cache.push(fn);
}
}
可以再定义一个installEvent函数,传入一个对象,里面的对象都装载发布订阅功能:
var event = {
cache : [], //存放订阅消息
pub : function(){ //发布消息
for(var i= 0;fn;fn = this.cache[i++]){
fn.call(this.arguments)
}
},
sub : function(fn){ //增加订阅者
this.cache.push(fn);
}
}
//
var installEvent = function(obj) {
for (var i in PubSub) {
obj[i] = PubSub[i];
}
};
var day = {}
installEvent(day);
我们已经实现了一个最简单的发布订阅模式,但还存在一些问题。我们看到了订阅者接收到发布者发布的每条消息,所以我们需要增加一个topic,让订阅者订阅自己感兴趣的内容。
var event = {
cache:[],
publish:function(topic, args, scope){
if(this.cache[topic]){
var cachetopic = this.cache[topic],
i = cachetopic.length - 1;
for(i;i>=0;i-=1){
cachetopic[i].call( this, args );
}
}
},
subscribe:function(topic, callback){
if(!this.cache[topic]){
this.cache[topic] = [];
}
this.cache[topic].push(callback);
return [topic, callback]
}
}
var installEvent = function(obj) {
for (var i in event) {
obj[i] = event[i];
}}
var day = {}
installEvent(day);
day.subscribe('天气', function(wind) {
console.log('风力:'+ wind);
})
day.publish('天气', "8级风");
现在订阅者可以根据自己的需求订阅事件了。
全局发布订阅
回想上面的发布订阅,发现还有一些不足之处:
- 我们给每个发布者对象都添加了pub,sub方法,以及一个缓存数组。者其实是一种资源的浪费。
- 订阅者和发布者之间还是存在着耦合性,订阅者在订阅事件还是要知道发布者的名字
day.subscribe('天气', function(wind) {
console.log('风力:'+ wind);
})
如果订阅者还要订阅多个发布者,意味着还要订阅多个事件。
怎样能避免这种情况呢?发布订阅模式可以用一个全局的event对象来实现,这样订阅者并不需要了解消息来自哪个发布者,发布者亦然不需要知道谁订阅了事件,Event作为一个类似“中介者”,来沟通二者。
var Events = (function (){
var cache = {},
/**
* Events.publish
* e.g.: Events.publish("/Article/added", [article], this);
*
* @class Events
* @method publish
* @param topic {String}
* @param args {Array}
* @param scope {Object} Optional
*/
publish = function (topic, args, scope) {
if (cache[topic]) {
var thisTopic = cache[topic],
i = thisTopic.length - 1;
for (i; i >= 0; i -= 1) {
thisTopic[i].apply( scope || this, args || []);
}
}
},
/**
* Events.subscribe
* e.g.: Events.subscribe("/Article/added", Articles.validate)
*
* @class Events
* @method subscribe
* @param topic {String}
* @param callback {Function}
* @return Event handler {Array}
*/
subscribe = function (topic, callback) {
if (!cache[topic]) {
cache[topic] = [];
}
cache[topic].push(callback);
return [topic, callback];
},
/**
* Events.unsubscribe
* e.g.: var handle = Events.subscribe("/Article/added", Articles.validate);
* Events.unsubscribe(handle);
*
* @class Events
* @method unsubscribe
* @param handle {Array}
* @param completly {Boolean}
* @return {type description }
*/
unsubscribe = function (handle, completly) {
var t = handle[0],
i = cache[t].length - 1;
if (cache[t]) {
for (i; i >= 0; i -= 1) {
if (cache[t][i] === handle[1]) {
cache[t].splice(cache[t][i], 1);
if(completly){ delete cache[t]; }
}
}
}
};
return {
publish: publish,
subscribe: subscribe,
unsubscribe: unsubscribe
};
}());
模块间的通信
上文中实现的发布订阅模式,是基于一个全局的event 对象,我们利用这个特性可以在模块间通信,两个模块可以不用知道对方的情况。
但如果模块很多,也使用了很多的发布订阅模式,模块之间的联系就很难维护。
全局事件的命名冲突
全局的发布订阅只有一个cache来存放消息名和回调,时间长了,就会出现事件名冲突所以,我们要给event对象提供命名空间。
小结
这里要提出的是,我们一直讨论的发布一订阅模式跟一些别的语言(比如Java)中的实现还是有区别的。在java中实现一个自己的发布一订阅模式通常会把订阅者对象自身当成引用传人发布者对象中,同时订阅者对艇需供,个名为诸如upaate的方法.供发布者对象在适合的时候调用,而在javascrip中。我们用注册回调函数的形式来代替传统的发 布一订阅模式,显得更加优雅和简单。另外,在javasrnpt中。 我们无需去选择使用推模型还是拉模型.推模型是指在事件发生时发布者一次性把所有 更改的状态和数据都推送给订阅者。拉模型不同的地方是.发布者仅仅通知订阅者事件已经发生了此外发布者要提供一些公开的接口供订阅者来主动拉取数据,拉模数好处是可以让订阅者’按需获取” 但同时有可能让发布者变成一个’门户大开”的对象.同时增加了代码量和复杂度。刚好在lavaschpt中,argunents
可以很方便地表示参数列表,所以我们一般都会选择推模型,使用Function.Prototyoe.appiy
方法把所有参数推送给订阅者
实践中的发布订阅
let EventP=(() => {
let clientList={}, //订阅回调函数
listen, //监听器
trigger,//触发器
remove;
listen= (key,fn) => {
if(! clientList[key]){
clientList[key]=[];
}
clientList[key].push(fn);
};
trigger= (...rest) => {
let key=rest.shift(),
fns=clientList[key];
if(!fns||fns.length===0){
return false;
}
fns.forEach(function (val,index) {
val.apply(this,rest);
});
}
remove=(key,fn) => {
let fns=clientList[key];
if(!fns){
return false;
}
if(!fn){
fns && (fns.length =0);
}else{
fns.forEach(function (val,index) {
if(val==fn){
fns.splice(index,1);
}
});
}
};
return{
listen:listen,
trigger:trigger,
remove:remove,
}
})();
EventP.listen('console',(info) => {
console.log(info);
})
EventP.trigger('console','hello gcy'); //hello gcy
/**
* Events. Pub/Sub system for Loosely Coupled logic.
* Based on Peter Higgins' port from Dojo to jQuery
* https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js
*
* Re-adapted to vanilla Javascript
*
* @class Events
*/
var Events = (function (){
var cache = {},
/**
* Events.publish
* e.g.: Events.publish("/Article/added", [article], this);
*
* @class Events
* @method publish
* @param topic {String}
* @param args {Array}
* @param scope {Object} Optional
*/
publish = function (topic, args, scope) {
if (cache[topic]) {
var thisTopic = cache[topic],
i = thisTopic.length - 1;
for (i; i >= 0; i -= 1) {
thisTopic[i].apply( scope || this, args || []);
}
}
},
/**
* Events.subscribe
* e.g.: Events.subscribe("/Article/added", Articles.validate)
*
* @class Events
* @method subscribe
* @param topic {String}
* @param callback {Function}
* @return Event handler {Array}
*/
subscribe = function (topic, callback) {
if (!cache[topic]) {
cache[topic] = [];
}
cache[topic].push(callback);
return [topic, callback];
},
/**
* Events.unsubscribe
* e.g.: var handle = Events.subscribe("/Article/added", Articles.validate);
* Events.unsubscribe(handle);
*
* @class Events
* @method unsubscribe
* @param handle {Array}
* @param completly {Boolean}
* @return {type description }
*/
unsubscribe = function (handle, completly) {
var t = handle[0],
i = cache[t].length - 1;
if (cache[t]) {
for (i; i >= 0; i -= 1) {
if (cache[t][i] === handle[1]) {
cache[t].splice(cache[t][i], 1);
if(completly){ delete cache[t]; }
}
}
}
};
return {
publish: publish,
subscribe: subscribe,
unsubscribe: unsubscribe
};
}());
PubSubJS是一个标准的 发布/订阅库,用JavaScript编写。
PubSubJS具有同步解耦功能,
对于风险性,PubSubJS还支持同步主题发布。
这可以在某些环境(浏览器,而不是全部)中加快速度,但也可能导致一些非常难以推理的程序,其中一个主题会触发在同一执行链中发布另一个主题。
PubSubJS主要在单个进程中使用,并不适用于多进程应用程序(如Node.js -具有多个子进程的群集)。
如果您的Node.js应用程序是一个单独的进程应用程序,就可以用。
如果它是一个多进程应用程序,你可以使用redis Pub / Sub
主要特征
- 不依赖关系同步去耦
- ES3兼容。
PubSubJS应该能够运行到任何可以执行JavaScript的地方。浏 - AMD / CommonJS模块支持
- 不修改订阅者(jQuery自定义事件修改订阅者)
- 易于理解和使用(由于同步解耦)
- 小于1kb
class event {
constructor(){
this.publish = publish;
this.subscribe = subscribe;
this.unsubscribe = unsubscribe;
}
caches = {};
/**
* Events.publish
* e.g.: Events.publish("/Article/added", [article], this);
*
* @class Events
* @method publish
* @param topic {String}
* @param args {Array}
* @param scope {Object} Optional
*/
publish(topic, args, scope){
if(caches[topic]){
let thisTopic = cache[topic],
i = thisTopic.length-1;
for(i; i>=0; i-=1){
thisTopic[i].apply( scope || this,args || [])
}
}
}
/**
* Event.subscribe
* e.g.: Events.subscribe("/Article/added", Articles.validate)
*
* @class Events
* @method subscribe
* @param topic {String}
* @param callback {function}
* @return event hander {Array}
*/
subscribe(topic, callback){
if(!caches[topic]){
caches(topic) = [];
caches[topic].push(callback);
return [topic, callback];
}
}
/**
* Event.unsubscribe
* e.g.: Events.unsubscribe( [article], Articles.validate)
*
* @class Events
* @method unsubscribe
* @param handle {Array}
* @param competely {boolean}
* @return {type, discription}
*
*/
unsubscribe(handle, competely){
let t = handle[0],
i = cache[t].length - 1;
if (cache[t]) {
for (i; i >= 0; i -= 1) {
if (cache[t][i] === handle[1]) {
cache[t].splice(cache[t][i], 1);
if(completly){ delete cache[t]; }
}
}
}
}
}