第一部分: 发布订阅模式简介
发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在javascript开发中,一般用事件模型来替代传统的发布—订阅模式。
发布—订阅模式可以广泛应用于异步编程中,是一种替代传递回调函数的方案。比如,可以订阅ajax请求的error、success等事件。或者如果想在动画的每一帧完成之后做一些事情,可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布—订阅模式,就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点
第二部分:发布订阅模式在DOM编程操作过程中的使用
发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们
实际上,前端开发中常用的,在DOM节点上面绑定事件函数,就属于发布—订阅模式
document.body.addEventListener('click',function(){ alert(2); },false); document.body.click();
如果需要监控用户点击document.body的动作,但是没办法预知用户将在什么时候点击。所以订阅document.body上的click事件,当body节点被点击时,body节点便会向订阅者发布这个消息。
同理,其实DOM中的很多事件操作都是采用的这个原理
document.body.addEventListener('keyup',function(){ alert(2); },false); document.body.keyup(); document.body.addEventListener('mousedown',function(){ alert(2); },false); document.body.mousedown();
还可以为单个事件添加多个监听功能,
document.body.addEventListener('click',function(){ console.log(2); },false); document.body.addEventListener('click',function(){ console.log(3); },false); document.body.addEventListener('click',function(){ console.log(4); },false); document.body.click(); //模拟用户点击 // 2, 3, 4
第三部分:发布订阅模式的其他使用场景
假设有一个店铺,出售Iphone,会有多种型号的Iphone出售,而消费者也会有不同的需求,如果每个消费者都要来向店员询问自己需要的款型的价格,那么这是一个很低效的行为,因为消费者最关心的就是型号和价格,这样用发布订阅模式就最合适不过了
const shop = {}; // 首先定义一个商铺 shop.list = []; // 定义商铺里的商品信息列表 shop.listen = function(fn) { // 添加订阅者 this.list.push(fn); // 将订阅的商品添加进入商品心里列表 } shop.sell = function(){ for( var i = 0, fn; fn = this.list[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是发布消息参数 } } // 这是来了一个顾客询问手机的价格,那么 shop.listen(function(iphone, price) { console.log('手机型号' + iphone); console.log('价格' + price) }) // 发布消息,本店卖IphoneX, 价格7000 shop.sell('IphoneX', 7000); shop.sell('Iphone11', 9000); // 输出 手机型号IphoneX, 价格7000 // 输出 手机型号Iphone11, 价格9000
现在我们已经实现了一个最简单的发布订阅模式了,但这里还存在一些问题。订阅者接收到了发布者发布的每个消息,如果我只想买Iphone11,我是不关心IphoneX的价格的,但是发布者把IphoneX的信息也推送给了我,这对我来说是不必要的困扰。所以有必要增加一个标示key,让订阅者只订阅自己感兴趣的消息。改写后的代码如下:
const shop = {}; // 首先定义一个商铺 shop.list = {}; // 定义商铺里的商品信息列表 shop.listen = function(key, fn) { // 添加订阅者 if ( !this.list[key] ){ // 如果没有订阅,创建一个缓存列表 this.list[key] = []; } this.list[key].push( fn ); // 订阅的消息添加进消息缓存列表 } shop.sell = function(){ const key = Array.prototype.shift.call( arguments );// 取出消息 const fns = this.list[ key ]; // 取出该消息对应的回调函数集合 if ( !fns || fns.length === 0 ){ // 如果没有订阅该消息,则返回 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是参数 } } // 这是来了一个顾客询问手机的价格,那么 shop.listen('IphoneX', function(price) { console.log('价格' + price) }) // 这是来了一个顾客询问手机的价格,那么 shop.listen('Iphone11', function(price) { console.log('价格' + price) }) // 发布消息,本店卖IphoneX, 价格7000 shop.sell('IphoneX', 7000); shop.sell('Iphone11', 9000); // 输出 价格7000 // 输出 价格9000
依照上面的例子,我们就可以写一个基于对象的发布订阅的模型了
const event = { clientList: [], listen: function( key, fn ){ if ( !this.clientList[ key ] ){ this.clientList[ key ] = []; } this.clientList[ key ].push( fn ); // 订阅的消息添加进缓存列表 }, trigger: function(){ var key = Array.prototype.shift.call( arguments ), // (1); fns = this.clientList[ key ]; if ( !fns || fns.length === 0 ){ // 如果没有绑定对应的消息 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments } }, remove: function(key, fn) { var fns = this.clientList[ key ]; if ( !fns ){ // 如果key 被人订阅,则直接返回 return false; } if ( !fn ){ // 如果没有传入具体函数,表示需要取消所有订阅 fns && ( fns.length = 0 ); }else{ for ( var l = fns.length - 1; l >=0; l-- ){ var _fn = fns[ l ]; if ( _fn === fn ){ fns.splice( l, 1 ); // 删除订阅者的回调函数 } } } } };
// 下面是一个基于class的发布订阅模式的模版,考虑到了边界条件和匿名函数,属于一个比较完整的实现
class Pubsub {
constructor () {
}
list = {};
// 添加消息监听的方法
subscribe (topic, func) {
if (typeof topic !== 'string') {
throw 'topic为字符串类型'
}
if (typeof func !== 'function') {
throw 'func为函数类型'
}
const list = this.list;
if (!list[topic]) {
list[topic] = [];
}
list[topic].push(func);
// 为了防止匿名函数的影响,在添加时将取消监听的方法返回
return () => this.unsubscribe(topic, func);
}
// 发布消息的方法
publish (topic, data) {
if (typeof topic !== 'string') {
throw 'topic必须是字符串类型'
}
const list = this.list;
if(!list[topic]) {
throw '不存在该事件的监听'
} else {
list[topic].forEach((func)=>{
func.call(this, data)
})
}
}
// 移除消息监听的方法
unsubscribe (topic, func){
if(typeof topic !== 'string') {
throw 'topic为字符串类型'
}
if(func && (typeof func !== 'function')) {
throw 'func为函数类型'
}
const list = this.list;
if(!list[topic]) {
throw '不存在该topic监听'
}
if(!func) { // 如果没有第二个参数,就移除所有的监听事件
if(list[topic]) {
delete list[topic]
}
} else {
if(!list[topic].includes(func)) {
throw '要移除的事件不存在'
} else {
const index = list[topic].findIndex(item => item === func);
list[topic].splice(index, 1);
if(list[topic].length === 0) {
delete list[topic]
}
}
}
}
}