什么是发布 — 订阅模式
发布 — 订阅模式,它定义程序对象之间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知并执行相应操作。在日常生活中,常见的发布订阅模式有:订阅号,用户关注订阅号,内容创作者在平台发布内容后,平台遍历粉丝列表进行内容推送;销售中介,客户给销售人员留下了客户信息及联系方式,在新产品推出时,挨个给客户打电话进行推销,等等… 而发布订阅模式,一般由三类对象组成:
发布者 Publisher
信道中介 Event Channel
作为发布订阅的中介,需要缓存相应事件的订阅者列表,在发布者发布时遍历订阅者列表并通知它们
订阅者 Subscriber
订阅事件,并定义事件发布后的操作,向信道中介发起订阅
发布 — 订阅模式的应用
在上面也介绍到了发布订阅模式在日常生活中以及前端开发工作中的使用场景,例如原生 DOM 事件,暴露了订阅接口后,开发者不需要反复查询该事件是否完成;异步编程的请求中,开发者无需轮询 HTTP 请求的状态,可以通过订阅请求的 success、error、update 等事件。在编程风格上,发布订阅模式让两个对象在松耦合的情况下建立联系,不再需要显式的将接口调用硬编码耦合进另一个对象,发布者和订阅者发生各自代码的变更都不会影响到对方,下面来看看前端开发最常见的发布订阅模式应用: DOM 事件和自定义事件。
DOM 事件
由简入繁, DOM 事件是前端开发中最常见、简单的发布订阅模式应用
document .body.addEventListener( 'click' , function ( ) { alert(1 ); }, false ); document .body.addEventListener( 'click' , function ( ) { alert(2 ); }, false ); document .body.click();
自定义事件
由上述订阅号为例子实现一个简易的自定义事件,进阶发布订阅模式应用
const subscription = {};subscription.subscriber = []; subscription.subscribe = function (fn ) { this .subscriber.push(fn); }; subscription.publish = function ( ) { this .subscriber.forEach((fn ) => { fn.apply(this , arguments ); }); }; subscription.subscribe((article ) => { console .log(`fans1 receive ${article} ` ); }); subscription.subscribe((article ) => { console .log(`fans2 receive ${article} ` ); }); const article = 'article' ;subscription.publish(article);
通用发布 - 订阅模式
上面自定义事件的例子,仅能局限在单个订阅号场景
下面通过 key 将事件的发布和订阅进行隔离,实现通用的发布订阅模式
class EventEmitter { subscriber = {}; subscribe (key, fn ) { if (!Array .isArray(this .subscriber[key])) { this .subscriber[key] = []; } this .subscriber[key].push(fn); } unsubscribe (key, fn ) { const subscribers = this .subscriber[key] || []; this .subscriber[key] = subscribers.filter((_fn ) => _fn !== fn); } unsubscribeAll (key ) { this .subscriber[key] = []; } publish (key, ...args ) { const subscribers = this .subscriber[key] || []; if (subscribers.length === 0 ) { console .log("has't subscriber" ); } subscribers.forEach((subscriber ) => { subscriber.apply(this , args); }); } } const e = new EventEmitter();const publisher = { article1: 'article1' , article2: 'article2' , }; const subscriber1 = (article ) => { console .log(`fans1 receive ${article} ` ); }; const subscriber2 = (article ) => { console .log(`fans2 receive ${article} ` ); }; e.subscribe('event1' , subscriber1); e.subscribe('event1' , subscriber2); e.publish('event1' , publisher.article1); e.subscribe('event2' , subscriber1); e.publish('event2' , publisher.article1); e.unsubscribe('event1' , subscriber1); e.publish('event1' , publisher.article2); e.unsubscribeAll('event1' ); e.publish('event1' );
模块间通信
模拟如下场景,在一个中台型平台上可以接入多款应用,切换不同的应用时需要:
拉取新的应用信息
拉取新的应用设置
拉取用户对新应用的权限信息
重置系列缓存数据
而这些业务逻辑和操作又封装在各自的 store 或者组件里,如果没有应用发布订阅模式,可能代码是下面这种情况;如果有新增一个切换应用的入口,又需要 CV 一遍同样的代码;
function openApp ( ) { userStore.getPermission(); appStore.getAppInfo(); settingStore.getAppSetting(); dataStore.resetDataCache(); }
而如果应用了发布订阅模式,只需在各自的模块内执行订阅和发布操作即可,降低模块间的耦合度
如果新增切换应用的入口,也只需要新增一行“发布”操作的代码
export const event = new EventEmitter();export const EVENT_TYPE = { OPEN_APP: 'OPEN_APP' , }; event.subscribe(EVENT_TYPE.OPEN_APP, getPermission); event.subscribe(EVENT_TYPE.OPEN_APP, getAppInfo); event.subscribe(EVENT_TYPE.OPEN_APP, getAppSetting); event.subscribe(EVENT_TYPE.OPEN_APP, resetDataCache); function openApp ( ) { event.publish(EVENT_TYPE.OPEN_APP); }
观察者模式和发布 — 订阅模式
观察者模式和发布 — 订阅模式最大的区别在于:
观察者模式由具体目标(被观察对象)调度
发布 — 订阅模式由调度中心(信道中介)统一调度
发布 — 订阅模式比观察者模式多一个调度中心(信道中介)
也可以理解为发布 — 订阅模式是基于观察者模式进行通用化设计,松散耦合,灵活度更高
观察者模式和发布 — 订阅模式的前端实践分别有 Vue 的数据双向绑定和事件总线 EventBus
对 Vue 相关实现感兴趣可以看看以下几篇文章
小结
阅读完本文,我们对发布 — 订阅模式有了一个大致的了解。它的优点非常明显,时机和对象上的解耦,广泛应用于异步编程,同时帮助我们完成松耦合的代码编写。但也不是没有缺点,对象与对象之间的必要联系将被深埋在背后,可能会导致程序难以跟踪维护和理解。创建订阅者对象和存储列表需要消耗一定的时间和内存,而且存在很多订阅者订阅了一个事件之后,这个事件没有触发,而这块内存却一直被占用。优缺点都有,不要为了用而用,适合场景和业务的才是最好的。
设计模式系列文章推荐
掘金:前端 LeBron
知乎:前端 LeBron
持续分享技术博文,关注微信公众号 👇🏻