又叫观察者模式。

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知。

  • 发布订阅模式的作用
  • DOM事件
  • 自定义时间
  • 发布订阅的通用实现
  • 取消订阅事件
  • 真实例子
    • 网站订阅
  • 全局的发布订阅对象
  • 模块间通信

发布订阅模式的作用

一个例子,现实生活中,如果你要买房的话,会留下联系方式给房产中介,当你关注的楼盘一有消息,中介就会打电话通知你。
这就是现实生活中的发布订阅模式。可以发现,这种模式有显著的优点:

  • 订阅者不用时刻给发布者发起请求,而是由发布者在适当的时机(某个条件达成时)通知订阅者。
  • 两者不再有强耦合,当有新的订阅者出现时,只需要向发布者添加即可。

上述两点中,第一点可以广泛应用于异步编程中,是一种代替传递回调函数的方案。比如接口请求的success和error状态,或者动画每一帧完成时去调用一个函数。
第二点可以取代对象之间硬编码的通知机制,一个对象不再显式的调用另一个对象的某个接口。

DOM事件

在DOM节点上绑定事件函数,就是一种发布订阅模式。

  1. document.body.addEventListener('click', function() {
  2. console.log('1')
  3. }, false)

上述代码将click事件绑定在body中,当点击事件触发时,就会执行这个函数。
同样的,还可以随意新增删除订阅者。都不会影响发布者代码的编写。

  1. document.body.addEventListener('click', function() {
  2. console.log('2')
  3. }, false)
  4. document.body.addEventListener('click', function() {
  5. console.log('3')
  6. }, false)

会依次执行绑定在body上的事件,这些事件(订阅者)之间不会互相影响。

自定义事件

如何实现发布订阅模式?

  • 确定谁是发布者
  • 定义一个缓存列表,用来存放回调函数以通知订阅者
  • 发布消息时,遍历缓存列表,依次触发存放的回调函数

也可以向回调函数中传入一些参数,订阅者接受这些参数,并可以自行处理。

  1. let salesOffices = {} // 确定该对象作为发布者
  2. salesOffices.clientList = [] // 为发布者添加一个缓存列表
  3. salesOffices.listen = function(fn) { // 为发布者添加监听函数,用来向缓存列表中存放回调函数
  4. this.clientList.push(fn)
  5. }
  6. salesOffices.trigger = function() { // 为发布者添加触发函数,用来发布消息
  7. for(let i = 0, fn; fn = this.clientList[i++]) {
  8. fn.apply(this, arguments) // arguments是发布时携带的参数
  9. }
  10. }

向发布者订阅消息

  1. salesOffices.listen(function (price, meter) {
  2. console.log(price, meter, 'A')
  3. })
  4. salesOffices.listen(function (price, meter) {
  5. console.log(price, meter, 'B')
  6. })

发布者发布消息

  1. salesOffices.trigger(3000, 100) // 3000, 100, A
  2. salesOffices.trigger(4000, 80) // 4000, 80, B

以上就是一个简单的发布订阅模式的代码,但依然存在于一些问题,订阅者只想知道自己感兴趣的消息,而上述程序中,不论订阅者是否感兴趣,只要发布者触发了消息通知,所有订阅者都会收到消息。
所以为listen方法添加一个key,作为某一类订阅信息的标志。
添加了订阅类型的key后,原来clientList由数组改为对象,它应该具有以下数据结构

  1. let clientList = {
  2. type1: [],
  3. type2: [fn1, fn2]
  4. }

所以listen方法的逻辑应该改写为:

  1. salesOffices.listen = function(key, fn) {
  2. if(!this.clientList[key]) {
  3. this.clientList[key] = []
  4. }
  5. this.clientList[key].push(fn)
  6. }

而trigger方法则改写为:

  1. salesOffices.trigger = function () {
  2. let key = Array.prototype.shift.call(arguments)
  3. let fns = this.clientList[key] // 根据key值获取对应的订阅类型的缓存列表
  4. if(!fns || fns.length === 0) { // 如果未曾订阅过该类型的函数,则直接返回false
  5. return false
  6. }
  7. for(let i = 0, fn; fn = fns[i++]) { // 发布消息
  8. fn.apply(this, arguments)
  9. }
  10. }

由于arguments是一个类数组,本身并没有Array的方法,所以使用call来“借用”Array的shift方法,来获取函数中第一个形参。
这就实现了订阅者只会收到自己感兴趣的通知。

发布订阅模式的通用实现

将发布订阅的功能提取出来,放在一个单独的对象。

  1. let event = {
  2. clientList: {},
  3. listen: function(key, fn) {
  4. // 同以上listen逻辑
  5. },
  6. trigger: function() {
  7. // 同以上trigger逻辑
  8. }
  9. }

实现一个动态安装模式的函数,将event中的事件和值混入到目标对象中(这里有可能造成将原有目标函数中重名key的逻辑覆盖)。

  1. let installEvent = function(obj) {
  2. for(let key in event) {
  3. obj[key] = event[key]
  4. }
  5. }

下面定义一个要作为发布者的对象,然后通过installEvent为该对象添加发布订阅功能

  1. let salesOffices = {}
  2. installEvent(salesOffices)

这样salesOffices就作为一个发布订阅对象而存在了。

取消订阅事件

既然有订阅事件的函数,那么也应该有取消订阅事件的函数。根据上面通用发布订阅的实现,继续向event对象中新增一个remove函数。该函数接受两个参数。

  • key,需要取消订阅的类型
  • fn,需要取消的订阅函数

该函数的基本逻辑是

  • 检查在缓存列表中是否存在该类型的订阅
    • 如果没有,则返回false
  • 检查是否传入了fn
    • 如果存在,则取消对应的订阅
    • 如果不存在,则取消全部订阅
      1. let event = {
      2. remove: function(key, fn) {
      3. let fns = this.clientList[key] // 获取对应订阅类型的缓存列表
      4. if(!fns) {
      5. return false
      6. }
      7. if(!fn) { // 如果没有传入fn,就清除对应订阅类型的缓存列表
      8. fns.length = 0
      9. } else { // 如果传入fn,则遍历对应订阅类型的缓存列表,splice对应的订阅
      10. for(let l = fns.length - 1; l >= 0; l--) {
      11. let _fn = fns[l]
      12. if(_fn === fn) {
      13. fns.splice(1, l)
      14. }
      15. }
      16. }
      17. }
      18. }

真实例子

网站登录

全局的发布订阅对象

之前通用发布订阅中,存在一些问题

  • 为每个发布者都添加了listen和trigger方法,和一个clientList。有些浪费资源
  • 订阅者和发布者之间还是有一些耦合,订阅者需要知道发布者的名字才行。
    1. salesOffices.listen('typeA', function(price) { /* ... */ })
    如果订阅者还需要订阅其他发布者的信息,还需要额外再写一次
    1. otherOffices.listen('typeB', function(price) {})
    发布订阅模式可以使用全局的Event对象来实现。
    1. let Event = (function () {
    2. let clientList = {},
    3. listen,
    4. trigger,
    5. remove
    6. // listen trigger remove相关函数逻辑
    7. return {
    8. listen,
    9. trigger,
    10. remove
    11. }
    12. })()

剩余8.9-