javascript中的发布者与订阅者
javascript中的发布者与订阅者介绍发布者与订阅者模式实现发布—订阅模式功能代码封装取消订阅事件开发中遇到的发布--订阅功能全局--发布订阅对象代码封装理解模块间通信介绍发布者与订阅者模式之前分析vue双向绑定原理的时候,提到一个订阅器Dep,他主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数。原理同javascript中的观察者模式javascript中的发布–订阅模...
javascript中的发布者与订阅者
介绍发布者与订阅者模式
之前分析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的时候我们该做点什么。
发布订阅模式的缺点:
创建订阅者需要消耗一定的时间和内存。
虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等。
实现发布—订阅模式功能
- 实现发布—订阅模式功能,我们经常用到的也是我们经常忽略的所谓的发布—订阅模式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请求,请求成功后需要做如下事情:
- 渲染数据。
- 使用数据来做一个动画。
那么我们以前肯定是如下写代码:
$.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 就是我们上面封装的全局-发布订阅模式对象的封装代码;
参考文章
更多推荐
所有评论(0)