发布-订阅模式

7,295 阅读6分钟

简介

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。JavaScript开发中我们一般用事件模型来代替传统的发布-订阅模式

案例介绍1

小明最近喜欢上吃老北京烧饼,可是到了卖烧饼的地方发现已经卖完了,而且排队的人还很多.幸运的是卖烧饼那个MM看小明长得帅,告诉小明等一会就有烧饼吃啦!可是小明现在还有约会要去,不知道烧饼能什么时候出锅,总不能因为吃烧饼而不去约会吧!这时候小明灵机一动,说烧饼MM把你电话给我吧!我先去忙,等会打电话问你烧饼好了没有。烧饼MM也没想太多,把电话给小明了。后来小龙也来买烧饼,情况跟小明差不多,小龙也把烧饼MM的电话要走了。可是问题就这来了,小明、小龙一会儿打一个电话给烧饼MM,导致烧饼MM很烦,辞职走了不干了。
通过上边的事情我们可以发现,存在好多问题
第一:卖烧饼的MM应该充当发布者
第二:小明小龙的电话应该保存在卖烧饼的用户列表中,如果卖烧饼的MM离职,这用户就会丢失
第三:实际上没有这么笨蛋的销售方式的

卖烧饼的店主可以把小明、小龙的电话记录下来,等店里有烧饼了在通知小龙小明来拿这就是所谓的发布-订阅模式,代码如下:

/*烧饼店*/        
var Sesamecakeshop={
    clienlist:[],//缓存列表
    addlisten:function(fn){//增加订阅者
        this.clienlist.push(fn);
    },
    trigger:function(){//发布消息
        for(var i=0,fn;fn=this.clienlist[i++];){
            fn.apply(this,arguments);
        }
    }
}

/*小明发布订阅*/
Sesamecakeshop.addlisten(function(price,taste){
    console.log("小明发布的"+price+"元,"+taste+"味道的");
});
/*小龙发布订阅*/
Sesamecakeshop.addlisten(function(price,taste){
    console.log("小龙发布的"+price+"元,"+taste+"味道的");
});        

Sesamecakeshop.trigger(10,"椒盐");

从代码中可以看出,只有小明,小龙预定了烧饼,烧饼店就可以发布消息告诉小龙与小明。但是有个问题不知道大家发现了没有。小明只喜欢椒盐味道的。而小龙只喜欢焦糖味道的。上面的代码就满足不了客户的需求,给客户一种感觉就是,不管我喜欢不喜欢,你都会发给我。如果发布比较多,客户就会感到厌烦,甚至会想删除订阅。下边是对代码进行改良大家可以看看。

/*烧饼店*/        
var Sesamecakeshop={
    clienlist:{},/*缓存列表*/
    /**
     * 增加订阅者
     * @key {String} 类型
     * @fn {Function} 回掉函数
     * */
    addlisten:function(key,fn){
        if(!this.clienlist[key]){
            this.clienlist[key]=[];
        }
        this.clienlist[key].push(fn);
    },
    /**
     * 发布消息
     * */
    trigger:function(){
        var key=[].shift.call(arguments),//取出消息类型
            fns=this.clienlist[key];//取出该类型的对应的消息集合
        if(!fns || fns.length===0){
            return false;
        }
        for(var i=0,fn;fn=fns[i++];){
            fn.apply(this,arguments);
        }
    },
    /**
     * 删除订阅
     * @key {String} 类型
     * @fn {Function} 回掉函数
     * */
    remove:function(key,fn){
        var fns=this.clienlist[key];//取出该类型的对应的消息集合
        if(!fns){//如果对应的key没有订阅直接返回
            return false;
        }
        if(!fn){//如果没有传入具体的回掉,则表示需要取消所有订阅
            fns && (fns.length=0);
        }else{
            for(var l=fns.length-1;l>=0;l--){//遍历回掉函数列表
                if(fn===fns[l]){
                    fns.splice(l,1);//删除订阅者的回掉
                }
            }
        }
    }
}

/*小明发布订阅*/
Sesamecakeshop.addlisten("焦糖",fn1=function(price,taste){
    console.log("小明发布的"+price+"元,"+taste+"味道的");
});
/*小龙发布订阅*/
Sesamecakeshop.addlisten("椒盐",function(price,taste){
    console.log("小龙发布的"+price+"元,"+taste+"味道的");
});        

Sesamecakeshop.trigger("椒盐",10,"椒盐");

Sesamecakeshop.remove("焦糖",fn1);//注意这里是按照地址引用的。如果传入匿名函数则删除不了        

Sesamecakeshop.trigger("焦糖",40,"焦糖");

删除的时候需要注意的是,如果订阅的时候传递的是匿名函数,删除的时候如果传入的也是匿名函数。则删除不了。因为删除时候是按照地址引用删除的。传进去的两个匿名函数,对应的地址引用是不同的。

案例介绍2

比如咱们常见的用户身份分别有不同的功能,超级管理员拥有最高权限,可以删除修改任意用户。而普通用户则只能修改自己的账户信息。首先是用户身份验证,验证通过之后对应功能才可以显示。

//登录发布-订阅模式
login={
    clienlist:{},/*缓存列表*/
    /**
     * 增加订阅者
     * @key {String} 类型
     * @fn {Function} 回掉函数
     * */
    addlisten:function(key,fn){
        if(!this.clienlist[key]){
            this.clienlist[key]=[];
        }
        this.clienlist[key].push(fn);
    },
    /**
     * 发布消息
     * */
    trigger:function(){
        var key=[].shift.call(arguments),//取出消息类型
            fns=this.clienlist[key];//取出该类型的对应的消息集合
        if(!fns || fns.length===0){
            return false;
        }
        for(var i=0,fn;fn=fns[i++];){
            fn.apply(this,arguments);
        }
    }
}
//超级管理员修改所有用户
var editall=(function(){
    login.addlisten("loginsucc",function(data){
        editall.setview(data);
    });
    return{
        setview:function(data){
            console.log(data);
            console.log("超级管理员修改所有用户");
        }
    }
})();

//仅仅修改自己
var editOwn=(function(){
    login.addlisten("loginsucc",function(data){
        editOwn.setview(data);
    });
    return{
        setview:function(data){
            console.log(data);
            console.log("仅仅修改自己");
        }
    }
})();

发布-订阅模式简单封装

var _Event=(function(){
    var clienlist={},
    addlisten,trigger,remove;
    /**
     * 增加订阅者
     * @key {String} 类型
     * @fn {Function} 回掉函数
     * */
    addlisten=function(key,fn){
        if(!clienlist[key]){
            clienlist[key]=[];
        }
        clienlist[key].push(fn);
    };
    /**
     * 发布消息
     * */
    trigger=function(){
        var key=[].shift.call(arguments),//取出消息类型
            fns=clienlist[key];//取出该类型的对应的消息集合
        if(!fns || fns.length===0){
            return false;
        }
        for(var i=0,fn;fn=fns[i++];){
            fn.apply(this,arguments);
        }
    };
    /**
     * 删除订阅
     * @key {String} 类型
     * @fn {Function} 回掉函数
     * */
    remove=function(key,fn){
        var fns=clienlist[key];//取出该类型的对应的消息集合
        if(!fns){//如果对应的key没有订阅直接返回
            return false;
        }
        if(!fn){//如果没有传入具体的回掉,则表示需要取消所有订阅
            fns && (fns.length=0);
        }else{
            for(var l=fns.length-1;l>=0;l--){//遍历回掉函数列表
                if(fn===fns[l]){
                    fns.splice(l,1);//删除订阅者的回掉
                }
            }
        }
    };
    return{
        addlisten:addlisten,
        trigger:trigger,
        remove:remove
    }
})();


_Event.addlisten("jianbing",function(d,all){
    console.log("发布的消息来自:"+d+",具体信息:"+all);
});
_Event.addlisten("jianbing",function(d,all){
    console.log("发布的消息来自:"+d+",具体信息:"+all);
})
_Event.trigger("jianbing","小小坤","前端工程师,擅长JavaScript,喜欢结交更多的前端技术人员,欢迎喜欢技术的你加QQ群:198303871")

总结

发布-订阅模式就是常说的观察者模式,在实际开发中非常有用。它的优点是为时间是解耦,为对象之间解构,它的应用非常广泛,既可以在异步编程中也可以帮助我们完成更松的解耦。发布-订阅模式还可以帮助我们实现设计模式,从架构上来看,无论MVC还是MVVC都少不了发布-订阅模式的参与。然而发布-订阅模式也存在一些缺点,创建订阅本身会消耗一定的时间与内存,也许当你订阅一个消息之后,之后可能就不会发生。发布-订阅模式虽然它弱化了对象与对象之间的关系,但是如果过度使用,对象与对象的必要联系就会被深埋,会导致程序难以跟踪与维护。