介绍发布者与订阅者模式

之前分析vue双向绑定原理的时候,提到一个订阅器Dep,他主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数。原理同javascript中的观察者模式
javascript中的发布–订阅模式又叫观察者模式。他定义了对象间 一种一对多的关系。让多个观察者对象同时监听某一个主题对象。当一个对象发生变化时。所有依赖于他的对象都讲发生变化。

现实生活中的发布–订阅者模式

  • A无意间看到了公众号B的文章,觉得文章不错。于是关注了公众号B。那么,当公众号B推送消息的时候,A就能接收到相关推送。
  • 公众号B有一个订阅者列表,里面保存着所有关注者,每当推送文章的时候,B会将消息推送给关注者列表中的每一个用户。
  • 如果A哪天对公众号B的文章不感兴趣了,不想接受他的推送消息。于是就取消关注,那么这个A信息将在该公众号B的关注列表中消失。等到该公众号B发布推送时,A就收不到消息了

发布订阅模式的优点:

1.支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。

2.发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变;同理,公众号B(发布者)只需要将文章的推送消息发给订阅者(关注者),他不用管订阅者(关注者)看还是不看,他只是告诉订阅者文章更新了;

就像我们平时发送的ajax请求,我们不会去管ajax的过程怎么样,我们发送ajax请求以后,只关注success和error的时候我们该做点什么。

发布订阅模式的缺点:

创建订阅者需要消耗一定的时间和内存。
虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等。

实现发布—订阅模式功能

  1. 实现发布—订阅模式功能,我们经常用到的也是我们经常忽略的所谓的发布—订阅模式Dom事件。仔细想想是不是?
	document.body.addEventListener('click',function(){
	  alert(2);
	},false);
	document.body.click(); // 模拟用户点击

在这里需要监控用户点击document.body的动作,但是没办法预知用户将在什么时候点击。所以订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息

2.自定义事件
 除了DOM事件,还会经常实现一些自定义的事件,这种依靠自定义事件完成的发布—订阅模式可以用于任何javascript代码中

下面是实现发布—订阅模式的步骤:

1、先要指定好谁充当发布者(公众号B)

2、然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(关注者列表)

3、最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历关注者列表,挨个发消息)

var PublicNum = {}; // 定义发布者
PublicNum.list = []; // 缓存列表 存放订阅者回调函数

// 添加订阅者
PublicNum.listen = function(fn){
	PublicNum.list.push(fn)
}

// 发布消息
PublicNum.trigger = function(){
    for(var i = 0,fn; fn = this.list[i++];) {
        fn.apply(this,arguments); 
    }
}
// 关注者A
PublicNum.listen(function(title, type){
	console.log('题目是:'+title)
	console.log('喜欢的类型是:'+type)
})
//关注者B
PublicNum.listen(function(title, type){
	console.log('题目是:'+title)
	console.log('喜欢的类型是:'+type)
})

PublicNum.trigger('西红柿', '生活')
PublicNum.trigger('保时捷', '跑车')

运行结果如下:
在这里插入图片描述
看运行结果,我们可以发现,订阅者收到的发布者的每一个消息。但是呢,对于A来说,他只想接收生活类型的文章消息,不想接收跑车类型的消息。为此,我们需要对代码进行改造。所以我们有必要添加一个key,让订阅者只订阅自己感兴趣的消息。代码如下:

var PublicNum = {}; // 定义发布者
PublicNum.list = []; // 缓存列表 存放订阅者回调函数

// 添加订阅者
PublicNum.listen = function(key,fn){
 if(!this.list[key]) {
        // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
        this.list[key] = []; 
    }
	this.list[key].push(fn)
}

// 发布消息
PublicNum.trigger = function(){
// arguments是一个类数组,虽然他也有下标,但并非真正的数组
//所以他不能和数组一样进行排序添加之类的操作
 var key = Array.prototype.shift.call(arguments); // 取出消息类型名称
    var fns = this.list[key];  // 取出该消息对应的回调函数的集合

    // 如果没有订阅过该消息的话,则返回
    if(!fns || fns.length === 0) {
        return;
    }
    for(var i = 0,fn; fn = fns[i++];) {
        fn.apply(this,arguments); 
    }
}
// 关注者A
PublicNum.listen('生活',function(title){
	console.log('题目是:'+title)
	
})
//关注者B
PublicNum.listen('跑车',function(title){
	console.log('题目是:'+title)
})

PublicNum.trigger( '生活','西红柿')
PublicNum.trigger( '跑车','保时捷')

运行结果如下:这样,订阅者就能获取他说喜欢的内容了
在这里插入图片描述

代码封装

通过封装通用发布–订阅方法,我们可以让所有普通的对象都拥有发布–订阅的功能。
1.首先,我们把发布–订阅的功能提取出来,单独放在一个对象里面:即listen和trigger

var event = {
    List: [],
    listen: function( key, fn ){
        if ( !this.List[ key ] ){
            this.List[ key ] = [];
        }
        this.List[ key ].push( fn ); // 订阅的消息添加进缓存列表
  	},
    trigger: function(){
         var key = Array.prototype.shift.call( arguments ), // 取出消息类型名称
         fns = this.List[ key ];  // 取出该消息对应的回调函数的集合
         if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息
                return false;
          }
         for( var i = 0, fn; fn = fns[ i++ ]; ){
               fn.apply( this, arguments ); 
            }
      	}
  };

我们在定义一个initEvent函数,这个函数使所有的普通对象都具有发布订阅功能,如下代码:

	var initEvent = function(obj) {
	    for(var i in event) {
	        obj[i] = event[i];
	    }
	};

我们测试一下:还如上面的例子:为对象PublicNum添加发布–订阅功能

var PublicNum ={}
initEvent (PublicNum)
// 关注者A
PublicNum.listen('生活',function(title){
	console.log('题目是:'+title)
})
//关注者B
PublicNum.listen('跑车',function(title){
	console.log('题目是:'+title)
})

PublicNum.trigger( '生活','西红柿')
PublicNum.trigger( '跑车','保时捷')

取消订阅事件

有时候,也许需要取消订阅事件的功能。例如:A不想接收某个类型的消息推送。代码如下:

event.remove = function(key,fn){
    var fns = this.List[key];
    // 如果key对应的消息没有订阅过的话,则返回
    if(!fns) {
        return false;
    }
    // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
    if(!fn) {
        fn && (fns.length = 0);
    }else {
        for(var i = fns.length - 1; i >= 0; i--) {
            var _fn = fns[i];
            if(_fn === fn) {
                fns.splice(i,1); // 删除订阅者的回调函数
            }
        }
    }
};

测试代码如下:

var PublicNum ={}
initEvent (PublicNum)
// 关注者A
PublicNum.listen('生活',fn1 = function(title){
	console.log('题目是:'+title)
})
//关注者A
PublicNum.listen('生活',fn2 = function(title){
	console.log('题目是:'+title)
})

PublicNum.remove( '生活',fn1')
PublicNum.trigger( '生活','保时捷')

在这里插入图片描述

开发中遇到的发布–订阅功能

网站登录
  假如正在开发一个商城网站,网站里有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。这是针对具体实现编程的典型例子,针对具体实现编程是不被赞同的。
 等到有一天,项目中又新增了一个收货地址管理的模块,在最后部分加上这行代码:

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

用发布—订阅模式重写之后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件。当登录成功时,登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解它们的内部细节。改进后的代码如下:

$.ajax('http://xx.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(){ // nav 模块
    login.listen( 'loginSucc', function( data ){
        nav.setAvatar( data.avatar );
    });
    return {
        setAvatar: function( avatar ){
            console.log( '设置nav 模块的头像' );
        }
    }
})();

如上所述,随时可以把setAvatar的方法名改成setTouxiang。如果有一天在登录完成之后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,代码如下:

var address = (function(){ // nav 模块
    login.listen( 'loginSucc', function( obj ){
        address.refresh( obj );
    });
    return {
        refresh: function( avatar ){
            console.log( '刷新收货地址列表' );
        }
    }
})();

全局–发布订阅对象代码封装

我们再来看看我们传统的ajax请求吧,比如我们传统的ajax请求,请求成功后需要做如下事情:

  1. 渲染数据。
  2. 使用数据来做一个动画。
    那么我们以前肯定是如下写代码:
$.ajax(“http://127.0.0.1/index.php”,function(data){
    rendedData(data);  // 渲染数据
    doAnimate(data);  // 实现动画 
});

假如以后还需要做点事情的话,我们还需要在里面写调用的方法;这样代码就耦合性很高,那么我们现在使用发布-订阅模式来看如何重构上面的业务需求代码;

$.ajax(“http://127.0.0.1/index.php”,function(data){
    Obj.trigger(‘success’,data);  // 发布请求成功后的消息
});
// 下面我们来订阅此消息,比如我现在订阅渲染数据这个消息;
Obj.listen(“success”,function(data){
   renderData(data);
});
// 订阅动画这个消息
Obj.listen(“success”,function(data){
   doAnimate(data); 
});

为此我们可以封装一个全局发布-订阅模式对象,如下代码:

var Event = (function(){
    var list = {},
          listen,
          trigger,
          remove;
          listen = function(key,fn){
            if(!list[key]) {
                list[key] = [];
            }
            list[key].push(fn);
        };
        trigger = function(){
            var key = Array.prototype.shift.call(arguments),
                 fns = list[key];
            if(!fns || fns.length === 0) {
                return false;
            }
            for(var i = 0, fn; fn = fns[i++];) {
                fn.apply(this,arguments);
            }
        };
        remove = function(key,fn){
            var fns = list[key];
            if(!fns) {
                return false;
            }
            if(!fn) {
                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("color",function(size) {
    console.log("尺码为:"+size); // 打印出尺码为42
});
Event.trigger("color",42);

理解模块间通信

我们使用上面封装的全局的发布-订阅对象来实现两个模块之间的通信问题;比如现在有一个页面有一个按钮,每次点击此按钮后,div中会显示此按钮被点击的总次数;如下代码:

<button id="count">点将我</button>
<div id="showcount"></div>

我们中的a.js 负责处理点击操作 及 发布消息;如下JS代码:

var a = (function(){
    var count = 0;
    var button = document.getElementById("count");
    button.onclick = function(){
        Event.trigger("add",count++);
    }
})();

b.js 负责监听add这个消息,并把点击的总次数显示到页面上来;如下代码:

var b = (function(){
    var div = document.getElementById("showcount");
    Event.listen('add',function(count){
        div.innerHTML = count;
    });
})();

下面是html代码如下,JS应用如下引用即可:

<!doctype html>
<html lang="en">
 <head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="global.js"></script>
 </head>
 <body>
    <button id="count">点将我</button>
    <div id="showcount"></div>
    <script src = "a.js"></script>
    <script src = "b.js"></script>
 </body>
</html>

如上代码,当点击一次按钮后,showcount的div会自动加1,如上演示的是2个模块之间如何使用发布-订阅模式之间的通信问题;

其中global.js 就是我们上面封装的全局-发布订阅模式对象的封装代码;
参考文章

Logo

前往低代码交流专区

更多推荐