聊聊设计模式(1):发布订阅模式

发布订阅模式

发布/订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 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]; }
                    }
                }
            }
             }


}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,036评论 29 470
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,622评论 2 17
  • 七月的西藏,平均气温也就在二十度出头。为了看日出,我们在纳木错的旁边搭了一个帐篷,然而晚上的时候,气温很低。 我们...
    靑檸阅读 193评论 0 1