简介
作为一种优秀的设计模式,发布订阅模式被广泛的应用在前端领域。举个例子,在 vue 的源码中,为了让数据劫持和视图驱动解耦就是通过架设一层消息管理层实现的,而这一层消息管理层实现的原理就是发布订阅模式。再比如 Redux、Vuex 这些当下比较流行的库基本上都离不开发布订阅模式。
现在我们举个例子来感受下发布订阅模式,比如有这么一个场景:到饭点了,你想找几个同事一起出去搓一顿。
- 方案A:你在组里挨个问一遍,问了一大圈之后,找到了几个也正好想要出去吃的人。
- 方案B:你在同事都在的群里发了条消息,问了一下谁想出去搓一顿的,然后同事A/B/C回复了你说要一起出去吃。
显而易见,方案B才是一个比较省事的方案。在这个简单的场景方案中,你就扮演着一个发布者(Publisher)的角色,而你的所有同事,都以订阅者(Subscriber)的身份接收着你发布的消息。

在介绍发布订阅模式的巧妙应用场景之前,先来分析一下该模式的优劣势。
代码实现可以前往 events 查看。
发布订阅模式的优势
- 松耦合
- 易维护
- 解决负载问题
- ...
松耦合
发布者不需要知道有多少订阅者,以及订阅者接收到消息之后会干什么,而订阅者也不需要关心发布者会在什么时候发布消息,两者相互独立运行。
易维护
得益于松耦合的特性,发布者和订阅者之间没有直接的逻辑往来,也使得逻辑变得清晰可维护,只需要关心内部的逻辑即可。
解决负载问题
在后端开发中消息中间件是用来解决写库高并发的常用手段,在前端同样可行。在并发执行量较高的场景下,可以考虑使用消息机制分流,避开执行高峰期,异步执行。
发布订阅模式的劣势
耦合度低是发布订阅模式最大优点,但同时也是它最大的缺点。
- 消息无状态
- 订阅者的数量不可控
- 发布者和订阅者的关系陌生
- ...
消息无状态
订阅者只会在接收到消息的时候作出响应,但是如果发布者的消息发布失败了,订阅者是不会知道的。
订阅者的数量不可控
因为发布者跟订阅者是一对多的关系,所以不会限制订阅者的数量。在发布者发布消息的时候,所有的订阅者都会收到对应的消息。如果订阅者过多,就很容易阻塞住进程,甚至造成 cpu 占用过大的情况。
发布者和订阅者的关系陌生
在发布订阅模式下,订阅者只认识消息,不认识发布者,所以任何发布者都可以发布指定的消息来通知订阅者,哪怕它是恶意伪装的。
巧妙的应用
介绍了一大堆,现在我们来看看如何应用。
- 前端曝光埋点
- 图片/模块懒加载
- 多场景触发
- 层级关系复杂的组件间通信
- ...
前端曝光埋点
前端曝光埋点应该可以说是家常便饭了,每一次新需求开发你都省不了曝光埋点。不同的开发进行曝光埋点的方式也不尽相同。下面来介绍几种方案:(为了更加形象,以下说的都是无限滚动列表曝光)
- 在接口返回的时候,一次性遍历列表,并发送曝光请求。如大家所想,这种方式严重丧失了曝光的意义,曝光数据没有参考价值。不过我确实碰到过很多同学依然用着这种“曝光方式”。
- 监听浏览器的滚动事件,滚动触发的时候获取列表的所有的节点,然后判断每个节点是否满足曝光条件进行曝光。这种方案的问题也很明显,如果列表变得很大,那么每次遍历的成本就变得很高,而且执行也会很频繁。
- 在上条方案的基础上我们来优化一版,我们可以给滚动事件添加一个节流器,然后再给已经曝光过的节点添加一个标识,这样每次遍历判断的列表长度就是可控的了
经过上面一番优化之后,曝光埋点的方案看上去应该比较合理了。但是我是一个比较懒的人,一看到每次滚动触发都需要主动去获取节点列表,还要去遍历整个列表就比较烦了。
现在我们用发布订阅模式的思想来设计一下这个问题。我们不妨把滚动触发的主体看成是发布者,然后把列表的每一项都当成是订阅者。当每次滚动触发的时候我们只是发布一则消息(event_explode)去通知列表中的每一项,由列表项(订阅者)自行来判断是否满足了曝光的条件并上报埋点,然后在曝光之后取消对这则消息的监听。
图片/模块懒加载
图片/模块按需加载是前端优化中比较常见的一种方案。其设计思路跟前端曝光埋点基本一致,这里不作重复介绍。
多场景触发
中后台开发有这么一个非常常见的场景:一个列表上同时有“修改”、“添加”和“删除”,在每次操作成功之后需要刷新列表。
比较常见的一种做法是把刷新列表的回调方法分别传递给“修改”、“添加”和“删除”模块,然后在操作成功之后执行回调函数(刷新列表)。还好只有三个操作,如果有七八个操作都会影响列表的数据,那就得把这个回调函数往五湖四海传了。 发布订阅模式松耦合的特性就比较完美的解决了这个问题,在每次操作成功之后只需要发布一则(event_refresh)消息,然后列表(订阅者)接收到这则消息之后自行进行刷新数据的操作。
前端工程化通常会设计一层 server 层用作接口请求封装。如果让 server 层继承我们的发布订阅模式,在操作接口请求返回成功的第一时间派发消息,就更好的解耦了操作模块和列表模块,操作模块都不需要关心操作成功之后需要干什么,因为 server 层已经帮它干了。
层级关系复杂的组件间通信
在日常开发中,特别是组件库开发过程中,用回调方法 or 事件经常是一件犯难的事情。组件间传递回调函数可以清晰的分析执行体来源,调用链路一目了然。然而在组件层级关系复杂的场景下,如果跨多层级传递回调函数,就会使得项目的耦合性很高,变得很难维护。另外如果组件间没有明显的关系,你就连传递回调函数的机会都没有了。
所以,如果组件间层级关系复杂,或者毫无关系的情况下,推荐使用发布订阅模式进行组件间通信。
发布订阅模式给我们带来很多便利的同时,也给我们带来了很多维护消息的成本,切忌不要滥用。