js设计模式5-发布订阅模式

本文详细介绍了发布-订阅模式的概念及其在实际生活中的应用,如售楼处短信通知和商城网站用户信息同步。通过示例展示了如何实现一个简单的发布-订阅模式,并逐步优化,包括增加消息过滤、全局事件对象以减少耦合以及解决命名冲突。最后,提出了全局事件的命名空间机制,以避免事件冲突并提高代码的可维护性。
摘要由CSDN通过智能技术生成

1.发布-订阅模式又叫做观察者模式,他定义对象件的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖他的对象都将得到通知

再现实生活中,送发短信通知就是一个典型的发布-订阅模式,小明,小红等购买者都是订阅者,他们订阅房子开售信息。售楼处作为发布者,会在何时的时候遍历花名册上的电话号码,依次给购房者发布消息

  1. 首先要指定好谁充当发布者(比如售楼处)
  2. 然后给发布者添加一个缓存列表,用于存放回掉函数以便通知订阅者(售楼处花名册)
  3. 最后发布消息时,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回掉函数(遍历花名册,挨个发短信)
var salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回掉函数
salesOffices.listen = function (fn) { // 增加订阅者
    this.clientList.push(fn); // 订阅的消息添加进缓存列表
}
salesOffices.trigger = function () { // 发布消息
    for (var i = 0, fn; fn = this.clientList[i++];) {
        fn.apply(this, arguments); // arguments是发布消息时带上的参数
    }
}
// 测试
salesOffices.listen(function(price,squareMeter){ // 小明订阅消息
    console.log(11,'价格='+price);
    console.log('squareMeter='+squareMeter);
})
salesOffices.listen(function(price,squareMeter){ // 小红订阅消息
    console.log('价格='+price);
    console.log('squareMeter='+squareMeter);
})
salesOffices.trigger(20000,88)
salesOffices.trigger(30000,110)

至此,我们已经实现了一个最简单的发布订阅模式,但还存在一个问题,比如小明只想买88平米的房子,但是发布者却把110平米的信息也推送给她,这对小明来说是不必要的困扰,所以我们有必要增加一个标示key,让订阅者只订阅自己感兴趣的消息

var salesOffices = {}; // 定义售楼处
salesOffices.clientList = {}; // 缓存列表,存放订阅者的回掉函数
salesOffices.listen = function (key,fn) { // 增加订阅者
    if(!this.clientList[key]){ // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
        this.clientList[key]=[]
    }
    this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
}
salesOffices.trigger = function () { // 发布消息
    var key=Array.prototype.shift.call(arguments); // 取出消息类型
    var fns=this.clientList[key];               // 取出该消息对应的回掉函数集合
    if(!fns || fns.length===0){ // 如果没有订阅该消息,则返回
        return false;
    }
    for (var i = 0, fn; fn = fns[i++];) {
        fn.apply(this, arguments); // arguments是发布消息时带上的参数
    }
}
// 测试
salesOffices.listen('squqreMeter88',function(price){ // 小明订阅消88平米房子信息
    console.log('价格='+price);
})
salesOffices.listen('squqreMeter110',function(price){ // 小红订阅110平米房子消息
    console.log('价格='+price);
})
salesOffices.trigger('squqreMeter88',20000)
salesOffices.trigger('squqreMeter110',30000)

现在订阅者可以只订阅自己感兴趣的事件了。假设现在小明又去另一个售楼处买房子,那么这段代码是否必须在另一个售楼处对象上重写一次呢,有没有办法可以让所有对象都拥有发布订阅功能呢,答案是肯定的,js给对象动态添加职责是理所当然,因此我们把发布订阅模式的功能提取出来,放在一个单独的对象内

var events = {
    clientList: {},
    listen: function(key,fn){
        if(!this.clientList[key]){ // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
            this.clientList[key]=[]
        }
        this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
    },
    trigger: function () { // 发布消息
        var key=Array.prototype.shift.call(arguments); // 取出消息类型
        var fns=this.clientList[key];               // 取出该消息对应的回掉函数集合
        if(!fns || fns.length===0){ // 如果没有订阅该消息,则返回
            return false;
        }
        for (var i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments); // arguments是发布消息时带上的参数
        }
    }
}
// 在定义一个installEvent函数,这个函数可以给所有对象都动态添加发布订阅功能
var installEvent = function(obj){
    for(var i in events) {
        obj[i]=events[i]
    }
}
var salesOffices = {};
installEvent(salesOffices);

salesOffices.listen('squqreMeter88',function(price){ // 小明订阅消88平米房子信息
    console.log('价格='+price);
})
salesOffices.listen('squqreMeter110',function(price){ // 小红订阅110平米房子消息
    console.log('价格='+price);
})
salesOffices.trigger('squqreMeter88',20000)
salesOffices.trigger('squqreMeter110',30000)

有时,我们也许需要取消订阅事件功能,现在我们给events对象添加remove方法

events.remove = function (key, fn) {
    var fns = this.clientList[key];
    if (!fns) { // 如果key对应的消息没有人订阅,则直接返回
        return false;
    }
    if (!fn) { // 如果没有传入具体的回掉函数,表示取消key对应消息的所有订阅
        fns && (fns.length = 0);
    } else {
        for (var i = fns.length - 1; i >= 0; i--) { // 反向遍历订阅的回掉函数列表
            var _fn = fns[i];
            if (_fn === fn) {
                fns.splice(i, 1); // 删除订阅者的回掉函数
            }
        }
    }
}
// 在定义一个installEvent函数,这个函数可以给所有对象都动态添加发布订阅功能
var installEvent = function (obj) {
    for (var i in events) {
        obj[i] = events[i]
    }
}
var salesOffices = {};
installEvent(salesOffices);

salesOffices.listen('squqreMeter88', fn1 = function (price) { // 小明订阅消88平米房子信息
    console.log('价格=' + price);
})
salesOffices.listen('squqreMeter110', fn2 = function (price) { // 小红订阅110平米房子消息
    console.log('价格=' + price);
})
salesOffices.remove('squqreMeter88', fn1) //删除小明的订阅
salesOffices.trigger('squqreMeter110', 30000)

我们再来看另外一个应用场景,假如我们正在开发一个商城网站,网站里有header头部、nav导航、消息列表、购物车等模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax异步请求获取用户登录信息,拿到用户名字和头像在显示到header里。至于ajax什么时候能成功返回用户信息,我们没办法确定,这个很像售楼处的例子。但现在不足以说服我们在此使用发布订阅模式,因为一部问题也可以通过回掉函数来解决,更重要的一点是,我们不知道除了header头部、、nav导航、消息列表、购物车之外,将来还有哪些模块需要使用这些用户信息。如果他们和用户信息模块产生了强耦合,比如下面:

login.succ(function(data){
    header.setAvatar(data.avatar); //设置header模块头像
    nav.setAvatar(data.avatar);//设置导航模块头像
    message.refresh(); // 刷新消息列表
    cart.refresh(); // 刷新购物车列表
})

现在登录模块是我们负责编写的,但我们必须了解header模块里设置头像的方法叫setAvatar、购物车模块里刷新方法叫refresh,这种耦合性会使程序变得僵硬,header模块不能随意在更改setAvatar的方法名,他自身也不能改成header1、header2,这是针对具体实现编程的典型例子,针对具体实现编程是不被赞同的。
等到有一天,项目又新增了一个收获地址管理的模块,这个模块本来是另一个同事所写的,而此时你在马来西亚度假,但是他却不得不给你打电话:“hi,登录之后麻烦刷新一下收货地址列表“,于是你又翻开3该月前写的登录模块,在最后部分加上这行代码:

login.succ(function(data){
    header.setAvatar(data.avatar); //设置header模块头像
    nav.setAvatar(data.avatar);//设置导航模块头像
    message.refresh(); // 刷新消息列表
    cart.refresh(); // 刷新购物车列表
    address.refresh();       // 增加这行代码
})

我们越来越疲于应付这些突如其来的业务要求,要么跳槽了事,要么必须重构这些代码
用发布订阅模式重写之后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件,当登录成功后,登录模块只需要发布登录成功的消息,而业务方接收到消息之后,就会开始进行各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解他们内部细节,改善后代码如下:

$.ajax('http://xxx.com?login',function(data){ // 登录成功
    login.trigger('loginSucc',data) //发布登录成功的消息
})
// 各模块监听登录成功的消息
var header = (function(){ // header模块
    login.listen('loginSucc',function(data){
        header.setAvatar(data.avatar);
    })
    return {
        setAvatar: function(data){
            console.log('设置header模块头像')
        }
    }
})()
var nav = (function(){ // header模块
    login.listen('loginSucc',function(data){
        nav.setAvatar(data.avatar);
    })
    return {
        setAvatar: function(data){
            console.log('设置nav模块头像')
        }
    }
})()

如上所述,我们随时可以把setAvatar的方法名改为setTouxiang。如果有一天在登录完成后,又增加一个刷新收获地址列表的行为,那么只要在收获地址模块里加上监听消息的方法即可,而这可以让开发模块同事自己完成,你作为登录模块开发中,永远不用在关心这些行为了,代码如下:

var address = (function(){ // header模块
    login.listen('loginSucc',function(data){
        address.setAvatar(data.avatar);
    })
    return {
        setAvatar: function(data){
            console.log('刷新收获地址列表')
        }
    }
})()

2.全局的发布-订阅对象

回想一下刚刚实现的发布订阅模式,我们给售楼处对象和登录对象都添加了订阅和发布的功能,这里还存在两个小问题

  1. 我们给每个发布者对象都添加了listen和trigger方法,以及一个缓存列表处理clientList,这其实是一种资源浪费
  2. 小明和售楼处对象还是存在一定的耦合性,小明至少要知道售楼处对象的名字是salesOffices,才能顺利的订阅到事件,见如下代码:
salesOffices.listen('squqreMeter88', fn1 = function (price) { // 小明订阅消88平米房子信息
    console.log('价格=' + price);
})

如果小明还关心300平米房子,而这套房子的买家是salesOffices2,这意味着小明还要开始订阅salesOffices2对象。

salesOffices2.listen('squqreMeter300', fn1 = function (price) { // 小明订阅消300平米房子信息
    console.log('价格=' + price);
})

其实在现实中,买房子未必要亲自去售楼处,我们只要把订阅的请求交给中介公司,而各大房产公司也只需要通过中介公司来发布房子信息。这样以来,我们不用关心消息来自哪个房产公司,我们在意的是否能顺利收到消息当然,为了保证订阅者和发布者能顺利通信,订阅者和发布者都必须知道这个中介公司。
同样在程序中,发布订阅模式可以用一个全局的Event对象来实现,订阅者不需要了解信息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event作为一个类似‘中介者’的角色,把订阅者和发布者联系起来。见如下代码:

var Event = (function () {
    var clientList = {};
    var listen;
    var trigger;
    var remove;
    listen = function (key, fn) {
            if (!clientList[key]) { // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
                clientList[key] = []
            }
            clientList[key].push(fn); // 订阅的消息添加进缓存列表
        },
        trigger = function () { // 发布消息
            var key = Array.prototype.shift.call(arguments); // 取出消息类型
            var fns = clientList[key]; // 取出该消息对应的回掉函数集合
            if (!fns || fns.length === 0) { // 如果没有订阅该消息,则返回
                return false;
            }
            for (var i = 0, fn; fn = fns[i++];) {
                fn.apply(this, arguments); // arguments是发布消息时带上的参数
            }
        }
    remove = function (key, fn) {
        var fns = clientList[key];
        if (!fns) { // 如果key对应的消息没有人订阅,则直接返回
            return false;
        }
        if (!fn) { // 如果没有传入具体的回掉函数,表示取消key对应消息的所有订阅
            fns && (fns.length = 0);
        } else {
            for (var i = fns.length - 1; i >= 0; i--) { // 反向遍历订阅的回掉函数列表
                var _fn = fns[i];
                if (_fn === fn) {
                    fns.splice(i, 1); // 删除订阅者的回掉函数
                }
            }
        }
    }
    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})()

Event.listen('squqreMeter88', function (price) { // 小明订阅消88平米房子信息
    console.log('价格=' + price);
})
Event.trigger('squqreMeter88', 200000); // 售楼处发布消息

3.全局事件的命名冲突

全局的发布订阅对象里只有一个clientList来存放消息名和回掉函数,大家都通过它来订阅发布各种消息,久而久之,难免会出现事件名冲突的情况,所以我们还可以给Event对象提供创建命名空间的功能。

var Event = (function () {
    var global=this;
    var Event;
    _default='default';
    Event = (function(){
        var _listen;
        var _trigger;
        var _remove;
        var _slice=Array.prototype.slice;
        var _shift=Array.prototype.shift;
        var _unshift=Array.prototype.unshift;
        var namespaceCache = {};
        var _create;
        var find;
        var each = function(ary,fn){
            var ret;
            for(var i=0,l=ary.length;i<l;i++){
                var n=ary[i];
                ret=fn.call(n,i,n);
            }
            return ret;
        }
        _listen = function (key, fn,cache) {
            if (!cache[key]) {
                cache[key] = []
            }
            cache[key].push(fn);
        },
        _trigger = function () {
            var cache = _shift.call(arguments);
            var key = _shift.call(arguments);
            var args=arguments;
            var _self=this;
            var ret;
            var stack = cache[key];
            if(!stack || !stack.length){
                return;
            }
            return each(stack,function(){
                return this.apply(_self,args)
            })
        };
        _remove = function (key,cache, fn) {
            if(cache[key]){
                if(fn) {
                    for (var i = cache[key].length; i >= 0; i--) {
                        if (cache[key][i] === fn) {
                            cache[key][i].splice(i, 1); // 删除订阅者的回掉函数
                        }
                    }
                }else {
                    cache[key]=[]
                }
            }
        };
        _create = function(namespace){
            var namespace = namespace || _default;
            var cache = {};
            var offlineStack=[]; // 离线事件
            ret = {
                listen:function(key,fn,last){
                    _listen(key,fn,cache);
                    if(offlineStack===null){
                        return;
                    }
                    if(last === 'last'){
                        offlineStack.length&&offlineStack.pop();
                    }else {
                        each(offlineStack,function(){
                            this();
                        });
                    }
                    offlineStack=null
                },
                one: function(key,fn,last){
                    _remove(key,cache);
                    this.listen(key,fn,last)
                },
                remove: function(key,fn){
                    _remove(key,cache,fn);
                },
                trigger: function(){
                    var fn;
                    var args;
                    var _self = this;
                    _unshift.call(arguments,cache);
                    args=arguments;
                    fn=function(){
                        return _trigger.apply(_self,args);
                    };
                    if(offlineStack){
                        return offlineStack.push(fn);
                    }
                    return fn();
                }
            };
            return namespace?(namespaceCache[namespace]?namespaceCache[namespace]:namespaceCache[namespace]=ret):ret;
        };
        return {
            create:_create,
            one:function(key,fn,last){
                var event=this.create();
                event.one(key,fn,last);
            },
            remove:function(key,fn){
                var event = this.create();
                event.remove(key,fn);
            },
            listen: function(key,fn,last){
                var event = this.create();
                event.listen(key,fn,last);
            },
            trigger: function(){
                var event=this.create();
                event.trigger.apply(this,arguments);
            }
        }
    })()
    return Event
})()

// 先发布后订阅
Event.trigger('click',1);
Event.listen('click',function(a){
    console.log(a)
})
// 使用命名空间
Event.create('namespace1').listen('click',function(a){
    console.log(a)
})
Event.create('namespace1').trigger('click',2)

Event.create('namespace2').listen('click',function(a){
    console.log(a)
})
Event.create('namespace2').trigger('click',3)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值