如何降低程序复杂度?掌握 JavaScript 设计模式之发布订阅模式

164 阅读5分钟

一、概述

发布订阅模式是一种常用的设计模式,它定义了一种一对多的关系,让多个订阅者对象同时监听某一个主题对象,当主题对象发生变化时,它会通知所有订阅者对象,使它们能够自动更新 。

二、优缺点

1. 优点:

  • 实现了发布者和订阅者之间的解耦,提高了代码的可维护性和复用性。
  • 支持异步处理,可以实现事件的延迟触发和批量处理。
  • 支持多对多的通信,可以实现广播和组播的功能。

2. 缺点:

  • 可能会造成内存泄漏,如果订阅者对象没有及时取消订阅,就会一直存在于内存中。
  • 可能会导致程序的复杂性增加,如果订阅者对象过多或者依赖关系不清晰,就会增加程序的调试难度。
  • 可能会导致信息的不一致性,如果发布者在通知订阅者之前或之后发生了变化,就会造成数据的不同步。

三、适用场景

发布订阅模式适用于以下场景:

  • 当一个对象的状态变化需要通知其他多个对象时,可以使用发布订阅模式来实现松耦合的通信
  • 当一个事件或消息需要广泛传播或分发给多个接收者时,可以使用发布订阅模式来实现高效的消息分发
  • 当一个系统需要支持异步处理或批量处理时,可以使用发布订阅模式来实现事件的延迟触发或批量触发

四、代码示例

在JavaScript中,实现发布订阅模式的基本思想是:

  • 定义一个发布者对象,它有一个缓存列表,用于存放订阅者对象的回调函数
  • 定义一个订阅方法,用于向缓存列表中添加回调函数
  • 定义一个取消订阅方法,用于从缓存列表中移除回调函数
  • 定义一个发布方法,用于遍历缓存列表,依次执行回调函数,并传递相关参数

下面是一个简单的发布订阅模式的代码示例 :

// 定义一个发布者对象
var pub = {
  // 缓存列表,存放订阅者回调函数
  list: {},
  // 订阅方法
  subscribe: function(key, fn) {
    // 如果没有该消息的缓存列表,就创建一个空数组
    if (!this.list[key]) {
      this.list[key] = [];
    }
    // 将回调函数推入该消息的缓存列表
    this.list[key].push(fn);
  },
  // 取消订阅方法
  unsubscribe: function(key, fn) {
    // 如果有该消息的缓存列表
    if (this.list[key]) {
      // 遍历缓存列表
      for (var i = this.list[key].length - 1; i >= 0; i--) {
        // 如果存在该回调函数,就从缓存列表中删除
        if (this.list[key][i] === fn) {
          this.list[key].splice(i, 1);
        }
      }
    }
  },
  // 发布方法
  publish: function() {
    // 获取消息类型
    var key = Array.prototype.shift.call(arguments);
	// 获取该消息的缓存列表
	var fns = this.list[key];
	// 如果没有订阅该消息,就返回
	if (!fns || fns.length === 0) {
  	return;
	}
	// 遍历缓存列表,执行回调函数
	for (var i = 0; i < fns.length; i++) {
  		fns[i].apply(this, arguments);
	}
  }
};

// 定义一个订阅者对象A 
var subA = function(name) { console.log('A收到了消息:' + name); };
// 定义一个订阅者对象B 
var subB = function(name) { console.log('B收到了消息:' + name); };

// A订阅了test消息 
pub.subscribe('test', subA);
// B订阅了test消息 
pub.subscribe('test', subB);

// 发布了test消息,传递了参数 'hello'
pub.publish('test', 'hello');
// 输出: 
// A收到了消息:hello 
// B收到了消息:hello

// A取消订阅了test消息 
pub.unsubscribe('test', subA);

// 发布了test消息,传递了参数 'world'
pub.publish('test', 'world');
// 输出: // B收到了消息:world

五、实现一个 Event Bus / Event Emitter

Event Bus / Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。

class EventEmitter {
  constructor() {
    // handlers是一个map,用于存储事件与回调之间的对应关系
    this.handlers = {}
  }

  // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下目标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []
    }

    // 把回调函数推入目标事件的监听函数队列里去
    this.handlers[eventName].push(cb)
  }

  // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  emit(eventName, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
      const handlers = this.handlers[eventName].slice()
      // 如果有,则逐个调用队列里的回调函数
      handlers.forEach((callback) => {
        callback(...args)
      })
    }
  }

  // 移除某个事件回调队列里的指定回调函数
  off(eventName, cb) {
    const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(cb)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  // 为事件注册单次监听器
  once(eventName, cb) {
    // 对回调函数进行包装,使其执行完毕自动被移除
    const wrapper = (...args) => {
      cb(...args)
      this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }
}


// 使用方法
const eventBus = new EventEmitter()
eventBus.on('test', (val) => {
    console.log(val, "===test")
})
eventBus.emit('test', 21) // 输出: 21,===test

六、总结

发布订阅模式是一种常用的设计模式,它可以实现对象间的松耦合通信,支持异步处理和多对多的通信。它也有一些缺点,比如可能会造成内存泄漏、程序复杂性增加和信息不一致性。在使用发布订阅模式时,需要注意合理地设计发布者和订阅者之间的关系,避免出现不必要的问题。