發(fā)布訂閱模式
發(fā)布/訂閱模式又叫觀察者模式纺非,它定義對象間的一種一對多的依賴關(guān)系蒂培,當(dāng)一個對象的狀態(tài)發(fā)生改變時贝润,所有依賴于它的對象都將得到通知。在 JavaScript 開發(fā)中钓账,我們一般用事件模型來替代傳統(tǒng)的發(fā)布/訂閱模式碴犬。
定義
發(fā)布訂閱模式,它定義了一種一對多的關(guān)系梆暮,讓多個觀察者對象同時監(jiān)聽某一個主題對象,這個主題對象的狀態(tài)發(fā)生變化時就會通知所有的觀察者對象绍昂,使得它們能夠自動更新自己啦粹。
使用發(fā)布訂閱模式的好處:
- 支持簡單的廣播通信,自動通知所有已經(jīng)訂閱過的對象窘游。
- 頁面載入后目標(biāo)對象很容易與觀察者存在一種動態(tài)關(guān)聯(lián)唠椭,增加了靈活性。
- 目標(biāo)對象與觀察者之間的抽象耦合關(guān)系能夠單獨擴展以及重用忍饰。
發(fā)布-訂閱的實現(xiàn)
var event = {
cache : [], //存放訂閱消息
pub : function(){ //發(fā)布消息
for(var i= 0;fn;fn = this.cache[i++]){
fn.call(this.arguments)
}
},
sub : function(fn){ //增加訂閱者
this.cache.push(fn);
}
}
可以再定義一個installEvent函數(shù)贪嫂,傳入一個對象,里面的對象都裝載發(fā)布訂閱功能:
var event = {
cache : [], //存放訂閱消息
pub : function(){ //發(fā)布消息
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);
我們已經(jīng)實現(xiàn)了一個最簡單的發(fā)布訂閱模式艾蓝,但還存在一些問題力崇。我們看到了訂閱者接收到發(fā)布者發(fā)布的每條消息,所以我們需要增加一個topic赢织,讓訂閱者訂閱自己感興趣的內(nèi)容亮靴。
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('風(fēng)力:'+ wind);
})
day.publish('天氣', "8級風(fēng)");
現(xiàn)在訂閱者可以根據(jù)自己的需求訂閱事件了。
全局發(fā)布訂閱
回想上面的發(fā)布訂閱于置,發(fā)現(xiàn)還有一些不足之處:
- 我們給每個發(fā)布者對象都添加了pub,sub方法茧吊,以及一個緩存數(shù)組。者其實是一種資源的浪費。
- 訂閱者和發(fā)布者之間還是存在著耦合性搓侄,訂閱者在訂閱事件還是要知道發(fā)布者的名字
day.subscribe('天氣', function(wind) {
console.log('風(fēng)力:'+ wind);
})
如果訂閱者還要訂閱多個發(fā)布者瞄桨,意味著還要訂閱多個事件。
怎樣能避免這種情況呢讶踪?發(fā)布訂閱模式可以用一個全局的event對象來實現(xiàn)芯侥,這樣訂閱者并不需要了解消息來自哪個發(fā)布者,發(fā)布者亦然不需要知道誰訂閱了事件俊柔,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
};
}());
模塊間的通信
上文中實現(xiàn)的發(fā)布訂閱模式雏婶,是基于一個全局的event 對象物赶,我們利用這個特性可以在模塊間通信,兩個模塊可以不用知道對方的情況留晚。
但如果模塊很多酵紫,也使用了很多的發(fā)布訂閱模式,模塊之間的聯(lián)系就很難維護错维。
全局事件的命名沖突
全局的發(fā)布訂閱只有一個cache來存放消息名和回調(diào)奖地,時間長了,就會出現(xiàn)事件名沖突所以赋焕,我們要給event對象提供命名空間参歹。
小結(jié)
這里要提出的是,我們一直討論的發(fā)布一訂閱模式跟一些別的語言(比如Java)中的實現(xiàn)還是有區(qū)別的隆判。在java中實現(xiàn)一個自己的發(fā)布一訂閱模式通常會把訂閱者對象自身當(dāng)成引用傳人發(fā)布者對象中,同時訂閱者對艇需供犬庇,個名為諸如upaate的方法.供發(fā)布者對象在適合的時候調(diào)用,而在javascrip中侨嘀。我們用注冊回調(diào)函數(shù)的形式來代替?zhèn)鹘y(tǒng)的發(fā) 布一訂閱模式臭挽,顯得更加優(yōu)雅和簡單。另外咬腕,在javasrnpt中欢峰。 我們無需去選擇使用推模型還是拉模型.推模型是指在事件發(fā)生時發(fā)布者一次性把所有 更改的狀態(tài)和數(shù)據(jù)都推送給訂閱者。拉模型不同的地方是.發(fā)布者僅僅通知訂閱者事件已經(jīng)發(fā)生了此外發(fā)布者要提供一些公開的接口供訂閱者來主動拉取數(shù)據(jù)涨共,拉模數(shù)好處是可以讓訂閱者’按需獲取” 但同時有可能讓發(fā)布者變成一個’門戶大開”的對象.同時增加了代碼量和復(fù)雜度纽帖。剛好在lavaschpt中,argunents
可以很方便地表示參數(shù)列表煞赢,所以我們一般都會選擇推模型,使用Function.Prototyoe.appiy
方法把所有參數(shù)推送給訂閱者
實踐中的發(fā)布訂閱
let EventP=(() => {
let clientList={}, //訂閱回調(diào)函數(shù)
listen, //監(jiān)聽器
trigger,//觸發(fā)器
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是一個標(biāo)準(zhǔn)的 發(fā)布/訂閱庫抛计,用JavaScript編寫。
PubSubJS具有同步解耦功能照筑,
對于風(fēng)險性吹截,PubSubJS還支持同步主題發(fā)布瘦陈。
這可以在某些環(huán)境(瀏覽器,而不是全部)中加快速度波俄,但也可能導(dǎo)致一些非常難以推理的程序晨逝,其中一個主題會觸發(fā)在同一執(zhí)行鏈中發(fā)布另一個主題。
PubSubJS主要在單個進程中使用懦铺,并不適用于多進程應(yīng)用程序(如Node.js -具有多個子進程的群集)捉貌。
如果您的Node.js應(yīng)用程序是一個單獨的進程應(yīng)用程序,就可以用冬念。
如果它是一個多進程應(yīng)用程序趁窃,你可以使用redis Pub / Sub
主要特征
- 不依賴關(guān)系同步去耦
- ES3兼容。
PubSubJS應(yīng)該能夠運行到任何可以執(zhí)行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]; }
}
}
}
}
}