聊聊設(shè)計模式(1):發(fā)布訂閱模式

發(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]; }
                    }
                }
            }
             }


}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末醒陆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子裆针,更是在濱河造成了極大的恐慌刨摩,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件世吨,死亡現(xiàn)場離奇詭異澡刹,居然都是意外死亡,警方通過查閱死者的電腦和手機耘婚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門罢浇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人沐祷,你說我怎么就攤上這事己莺。” “怎么了戈轿?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長阵子。 經(jīng)常有香客問我思杯,道長,這世上最難降的妖魔是什么挠进? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任色乾,我火速辦了婚禮,結(jié)果婚禮上领突,老公的妹妹穿的比我還像新娘暖璧。我一直安慰自己,他們只是感情好君旦,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布澎办。 她就那樣靜靜地躺著嘲碱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪局蚀。 梳的紋絲不亂的頭發(fā)上麦锯,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音琅绅,去河邊找鬼扶欣。 笑死,一個胖子當(dāng)著我的面吹牛千扶,可吹牛的內(nèi)容都是我干的料祠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼澎羞,長吁一口氣:“原來是場噩夢啊……” “哼髓绽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起煤痕,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梧宫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后摆碉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體塘匣,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年巷帝,在試婚紗的時候發(fā)現(xiàn)自己被綠了忌卤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡楞泼,死狀恐怖驰徊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情堕阔,我是刑警寧澤棍厂,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站超陆,受9級特大地震影響牺弹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜时呀,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一张漂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谨娜,春花似錦航攒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽币他。三九已至,卻和暖如春盆驹,著一層夾襖步出監(jiān)牢的瞬間圆丹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工躯喇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辫封,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓廉丽,卻偏偏與公主長得像倦微,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子正压,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理欣福,服務(wù)發(fā)現(xiàn),斷路器焦履,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,146評論 30 470
  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品拓劝,去做同樣的事情,實現(xiàn)同樣的效果;這時候需要使用工廠模式嘉裤。簡單...
    舟漁行舟閱讀 7,766評論 2 17
  • 七月的西藏郑临,平均氣溫也就在二十度出頭。為了看日出屑宠,我們在納木錯的旁邊搭了一個帳篷厢洞,然而晚上的時候,氣溫很低典奉。 我們...
    靑檸閱讀 211評論 0 1