1.定义
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式
2.发布订阅模式的作用
- 发布-订阅模式可以广泛用于异步编程中,这是一种替代回调函数的方案。比如可以在ajax请求后,或动画执行完成后做一些事情,那我们可以先订阅一个事件,然后在动画的每一帧完成之后发布这个事件。
- 发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起。
3.实现一个全局的发布-订阅对象
clientList是一个调度中心(缓存所有的订阅了的函数列表)lister方法用来订阅事件到clientList中去,通过key来缓存不同的事件trigger方法用来发布事件,通过arguments拿到发布事件时传递过来的key并拿到clientList中的函数,在传递参数去执行函数- remove方法用来移除
clientList中订阅的函数 - once方法只执行一次发布订阅后就移除订阅的函数
var Event = (function () {
var clientList = {};
var lister;
var trigger;
var remove;
var once;
lister = 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; i < fns.length; i++) {
var fn = fns[i];
fn.apply(this, arguments);
}
};
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
}
for (var i = 0; i < fns.length; i++) {
var _fn = fns[i];
if (_fn === fn) {
fns.splice(i, 1);
}
}
};
once = function (key, fn) {
var on = function () {
fn.apply(this, arguments);
remove(key, on);
};
lister(key, on);
};
return {
lister,
trigger,
remove,
once,
clientList,
};
})();
var test = function (a) {
console.log("发布了", a);
};
Event.lister("on", test);
// Event.once("on", test);
setTimeout(() => {
Event.trigger("on", "zelma");
}, 1000);
setTimeout(() => {
Event.trigger("on", "wanqian");
}, 2000);
4.发布-订阅模式的使用场景
- 当两个模块不能直接的显示调用时,就可以考虑使用发布-订阅模式
- 在一些有共同前提条件的的ajax请求时,请求完后可以用发布-订阅模式来解耦
业务场景:假如我们正在开发一个商城网站,网站里有header 头部、nav 导航、消息列表、购物车等模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax 异步请求获取用户的登录信息
普通写法
不适用发布-订阅模式,使用回调函数来实现
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) {
Event.trigger('loginSucc, data)
})
// 其他模块订阅登录成功消息
var header = (function(){ // header 模块
Event.listen( 'loginSucc', function( data){
header.setAvatar( data.avatar );
});
return {
setAvatar: function( data ){
console.log( '设置header 模块的头像' );
}
}
})();
var nav = (function(){ // nav 模块
Event.listen( 'loginSucc', function( data ){
nav.setAvatar( data.avatar );
});
return {
setAvatar: function( avatar ){
console.log( '设置nav 模块的头像' );
}
}
})();
如上所述,我们随时可以把setAvatar 的方法名改成setTouxiang。如果有一天在登录完成之后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为了。
var address = (function(){ // nav 模块
Event.listen( 'loginSucc', function( obj ){
address.refresh( obj );
});
return {
refresh: function( avatar ){
console.log( '刷新收货地址列表' );
}
}
})();
总结
发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。
发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。