下你所需,载你所想!
IT技术源码资料下载网站

逐步深入发布实例,代码深入了解实现

:其他软件 2020-09-07 11:07:12

逐步深入发布实例,代码深入了解实现

代码深入了解实现
1.简单实现
通过上面的例子,我们应该大致了解了发布-订阅者模式,那我们用代码模拟实现以下这个模式:
实现步骤:
先指定谁是发布者(售楼处)
然后给发布者添加一个缓存列表,用来存放回调函数以便通知订阅者(售楼处的花名册)
最后发布消息的时候,发布者遍历这个缓存列表,依次触发里面缓存的订阅函数(遍历花名册,发送售房消息)
另外,还可以在发布消息时,在回调函数里添加一些参数,订阅者可以接受到这些参数。这是非常必要的,比如售楼处可以在发给订阅者短信里加上房子的单价,面积等信息,订阅者可以接受到这个消息是进行各自的处理。
let salesOffice = {};//售楼处
salesOffice.clientList = [];//缓存列表,存放订阅者的回调函数(花名册)
salesOffice.listen = function(fn){//将订阅者添加到缓存列表
this.clientList.push(fn);
}
salesOffice.trigger = function(){//发布消息
if(!this.clientList){
return ;
}
for(let i = 0; i < this.clientList.length; ++i){//遍历缓存列表
let fn = this.clientList[i];
fn.apply(this, arguments);//arguments:发布消息带上的参数
}
}
salesOffice.listen((squareMeter,price)=>{//小明订阅的消息
console.log('面积=' + squareMeter);
console.log('价格=' + price);
})
salesOffice.listen((squareMeter,price)=>{//小红订阅的消息
console.log('面积=' + squareMeter);
console.log('价格=' + price);
})
salesOffice.trigger(88, 10000);//售楼处发布的消息
salesOffice.trigger(66, 20000);//售楼处发布的消息
到这里,我们已经实现了简单的发布-订阅模式。但这里还存在一些问题。订阅者接受到了发布者发布的所有消息,虽然小明只想购买88平米的房子,但发布者依然将66平米的房子消息也推送给了小明。这对小明来说是不必要的困扰,所有我们添加一个key值让订阅者,订阅他所感兴趣的消息。
let salesOffice = {};//售楼处
salesOffice.clientList = {};//缓存列表,存放订阅者的回调函数(花名册)
salesOffice.listen = function(key, fn){//将订阅者添加到缓存列表
if(!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn);
}
salesOffice.trigger = function(){//发布消息
let key = Array.prototype.shift.call(arguments);//取出参数中的key值
let fns = this.clientList[key];
if(!fns || !fns.length){//没有订阅这个消息就返回
return false;
}
for(let i = 0; i < fns.length; ++i){
let fn = fns[i];
fn(...arguments);
}
}
salesOffice.listen('squareMeter88', (price)=>{
console.log('价格=' + price);
});
salesOffice.listen('squareMeter66', (price)=>{
console.log('价格=' + price);
})
salesOffice.trigger('squareMeter88', 20000);
有没有办法让所有的对象都有发布-订阅功能呢?javascript作为一门解释执行的语言,给对象添加职责是理所当然的事情,将功能提前出来,放在一个单独的对象内。
let Event = {
clientList: {},
listen: function(key, fn){
if(!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger: function(){
let key = Array.prototype.shift.call(arguments);
let fns = this.clientList[key];
if(!fns && !fns.length){
return ;
}
for(let i = 0; i < fns.length; ++i){
let fn = fns[i];
fn(...arguments);
}
}
}
let intallEvent = function(obj){
for(let i in Event){
obj[i] = Event[i];
}
return obj;
}
let salesOffice = {};
intallEvent(salesOffice);
salesOffice.listen('squareMeter', (price)=>{
console.log('价格=' + price);
})
salesOffice.trigger('squareMeter', 20000)
有时候也需要取消订阅,当小明突然不想买房子了,避免继续接受到售楼处发过来的信息,小明需要取消之前订阅的消息,现在给Event添加remove方法。
let Event = {
clientList: {},
listen: function(key, fn){
if(!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger: function(){
let key = Array.prototype.shift.call(arguments);
let fns = this.clientList[key];
if(!fns || !fns.length){
return ;
}
for(let i = 0; i < fns.length; ++i){
let fn = fns[i];
fn(...arguments);
}
},
remove: function(key, fn){
let fns = this.clientList[key];
if(fns){
if(fn){
for(let i = fns.length - 1; i >= 0; --i){
if(fns[i] === fn){
fns.splice(i, 1);
}
}
}else{
fns.length = 0;
}
}
}
}
let installEvent = function(obj){
for(let i in Event){
obj[i] = Event[i];
}
}
let salesOffice = {};
installEvent(salesOffice);
salesOffice.listen('squareMeter88', (price)=>{
console.log('价格=' + price);
})
salesOffice.trigger('squareMeter88', 20000);
salesOffice.remove('squareMeter88');
salesOffice.trigger('squareMeter88', 20000);
2.全局发布订阅对象
刚刚实现的发布-订阅模式,依然有一些问题:1.给每个发布者对象添加listen,trigger,remove等属性,这其实是一种资源的浪费。2.小明和售楼处依然保持一定的耦合性,小明至少需要知道售楼处的名字(salesOffice),才能订顺利订阅到事件。
salesOffice.listen('squareMeter88', (price)=>{
console.log('价格=' + price);
})
如果小明想订阅200平米的房子,而这个房子的卖家是salesOffice2,这意味这小明需要订阅salesOffice2对象。
salesOffice2.listen('squareMeter88', (price)=>{
console.log('价格=' + price);
})
其实现实中,买房子未必需要亲自去售楼处,只要将订阅的请求交给中介公司,而各大房产公司也只需要通过中介公司发布房子的信息,这样一来,并不用关心消息是来自哪个公司,在意的是是否能顺利的收到消息。为了保证订阅者和发布者能正常通讯,订阅者和发布者必须要知道这个中介公司。
可以使用一个全局的Event对象来实现,订阅者不需要知道消息是来自那个发布者,发布者不需要知道消息推送给了哪个订阅者,Event作为一个类似"中介"的角色,将订阅者和发布者联系在了一起。
let Event = (function(){
let clientList = {},
listen,
trigger,
remove;

listen = function(key, fn){
if(!clientList[key]){
clientList[key] = [];
}
clientList[key].push(fn);
}
trigger = function(){
let key = Array.prototype.shift.call(arguments);
let fns = clientList[key];
if(!fns || !fns.length){
return ;
}
for(let i = 0; i < fns.length; ++i){
let fn = fns[i];
fn(...arguments);
}
}
remove = function(key, fn){
let fns = clientList[key];
if(fns){
if(fn){
for(let i = fns.length - 1; i >= 0; --i){
if(fns[i] === fn){
fns.splice(i, 1);
}
}
}else{
fns.length = 0;
}
}
}
return {
listen: listen,
trigger: trigger,
remove: remove
};
})();
Event.listen('squareMeter88', (price)=>{
console.log('价格=' + price);
});
Event.trigger('squareMeter88', 20000);
Event.remove('squareMeter88');
Event.trigger('squareMeter88', 30000);
模块通讯(高频面试考点)
上面实现了发布-订阅模式,它是基于Event全局对象的,可以利用它将两个封装良好的模块中进行通讯,这两个模块可以完全不知道对方的存在。
比如现在有两个模块,a模块有一个点击按钮,每次点击后,b模块的div中会显示按钮被点击的次数,使用发布-订阅模式,使得a,b模块可以在不改变封装性的条件下进行通讯。

0

let a = (function(){
let count = 0;
let button = document.querySelector('.click');
button.onclick = function(){
Event.trigger('clickButton', ++count);
}
})();
let b = (function(){
let div = document.querySelector('#num');
Event.listen('clickButton', (count)=>{
div.innerHTML = count;
})
})();
3.完整代码
我们这样封装了发布-订阅模式,还有一些问题:
有时候我们需要先发布后订阅(比如:当我们聊QQ的时候,你进入离线状态你的朋友给你发消息,你登录时依然可以接受到消息),这种需求在实际项目中是存在的,比如在商城网站中,获取到用户信息之后才能渲染用户导航模块,而获取用户信息的操作是一个ajax异步请求。当ajax请求成功返回之后会发布一个事件,在此之前订阅了此事件的用户导航模块可以接收到这些用户信息
  但是这只是理想的状况,因为异步的原因,不能保证ajax请求返回的时间,有时候它返回得比较快,而此时用户导航模块的代码还没有加载好(还没有订阅相应事件),特别是在用了一些模块化惰性加载的技术后,这是很可能发生的事情。也许还需要一个方案,使得的发布—订阅对象拥有先发布后订阅的能力。
  为了满足这个需求,要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ的未读消息只会被重新阅读一次,所以刚才的操作只能进行一次
全局的发布—订阅对象里只有一个clinetList来存放消息名和回调函数,大家都通过它来订阅和发布各种消息,久而久之,难免会出现事件名冲突的情况,所以还可以给Event对象提供创建命名空间的功能
给你们提供完整的代码,我也看了很久才理解到下面的这部分代码,虽然花了很多时间,但收获颇多。如果有推不通的可以给我留言。
let Event = function(){
let global = this,
Event,
_default = 'default';
Event = function(){
let _listen,
_trigger,
_remove,
_create,
each,
_self = this,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {};

each = function(stack, fn){
for(let i = 0; i < stack.length; ++i){
let n = stack[i];
fn.call(n);
}
}

_listen = function(key, fn, cache){
let fns = cache[key];
if(!fns){
cache[key] = [];
}
cache[key].push(fn)
}

_trigger = function(){
let cache = _shift.call(arguments);
let key = _shift.call(arguments);
let args = arguments;
let _self = this;
let fns = cache[key];
if(!fns || !fns.length){
return false;
}
return each(fns, function(){
return this.apply(_self, args);
})
}

_remove = function(key, cache, fn){
let fns = cache[key];
if(fns){
if(fn){
for(let i = 0; i < fns.length; i++){
if(fns[i] === fn){
fns.splice(i, 1);
}
}
}else{
fns.length = 0;
}
}
}

_create = function(namespace){
namespace = namespace || _default;
let listen,
trigger,
remove,
ret,
_self = this,
offlineStack = [],
cache = {};

ret = {
listen: function(key, fn, last){
_listen(key, fn, cache);
if(!offlineStack || !offlineStack.length){
offlineStack = null;
return ;
}
if(last === 'last'){

}else{
each(offlineStack, function(){
this();
})
}
offlineStack = null;
},
one: function(key, fn, last){
_remove(key, cache);
this.listen(key, fn, last);
},
trigger: function(){
_unshift.call(arguments, cache);
let args = arguments,
_self = this;
fn = function(){
_trigger.apply(_self, args);
}
if(offlineStack){
return offlineStack.push(fn);
}
return fn();
},
remove: function(key, fn){
_remove(key, cache, fn);
}
}
return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
}
return {
create: _create,
listen: function(key, fn, last){
let event = this.create();
event.listen(key, fn, last);
},
one: function(key, fn, last){
let event = this.create();
event.one(key, fn, last);
},
trigger: function(){
let event = this.create();
event.trigger.apply(this, arguments)
},
remove: function(){
let event = this.create();
event.remove(key, fn);
}
}
}();
return Event;
}();
Event.create('namespace1').listen('click', function(a){
console.log(a)
})
Event.create('namespace1').trigger('click', 1)
Event.create('namespace2').listen('click', function(a){
console.log(a)
})
Event.create('namespace2').trigger('click', 3)
Event.trigger('click',1);
Event.listen('click',function(a){
console.log(a); //输出:1
});
发布—订阅模式,也就是常说的观察者模式,它的优点非常明显,一为时间上的解耦,二为对象之间的解耦。应用也非常广泛,既可以用在异步编程中,也可以帮助完成更松耦合的代码编写。发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是MVC还是MVVM,都少不了发布—订阅模式的参与,而且javascript本身也是一门基于事件驱动的语言。

TAG: 代码,实现