加载中...
JavaScript设计模式 -- 5.发布订阅模式
发表于:2021-06-06 | 分类: 前端

定义

发布订阅模式又叫观察者模式。它定义对象间的一对多的依赖关系,当一个对象的状态发生变化时,所有订阅它的对象都将收到通知。js 中我们一般用事件模型来代替传统的观察者

应用场景

观察者模式在实际中有很多的应用场景,比如:

  • 房产中介发的看房通知,中介是发布者,而你是订阅者。
  • js 中的 DOM 事件
  • Node 中的 EventEmitter 类

下面我们就实现一个自定义的 EventEmitter, 为了方便查看,我们只实现增加监听器和触发监听器的功能

class EventEmitter {
  constructor() {
    this.listenersMap = {};
  }

  // 监听器
  on(eventName, listener) {
    this.listenersMap.hasOwnProperty(eventName)
      ? this.listenersMap[eventName].push(listener)
      : (this.listenersMap[eventName] = [listener]);
    return this;
  }

  // 添加单次监听器 listener 到名为 eventName 的事件。 当 eventName 事件下次触发时,监听器会先被移除,然后再调用。
  once(eventName, listener) {
    listener._once = true;
    this.listenersMap.hasOwnProperty(eventName)
      ? this.listenersMap[eventName].push(listener)
      : (this.listenersMap[eventName] = [listener]);
    return this;
  }

  // 触发
  emit(eventName) {
    const args = [].slice.call(arguments, 1);
    this.listenersMap[eventName] &&
      (this.listenersMap[eventName] = this.listenersMap[eventName].filter(
        (listener, index) => {
          listener.apply(null, args);
          return !listener._once;
        }
      ));
    return this;
  }

  off(eventName, fn) {
    this.listenersMap[eventName] &&
      (this.listenersMap[eventName] = this.listenersMap[eventName].filter(
        (listener, index) => {
          return listener !== fn;
        }
      ));
  }

  // listeners
  listeners(eventName) {
    return this.listenersMap[eventName];
  }
}
var myEmitter = new EventEmitter();
// 第一个监听器。
myEmitter.on("event", function firstListener() {
  console.log("第一个监听器");
});
// 第二个监听器。
myEmitter.on("event", function secondListener(arg1, arg2) {
  console.log(`第二个监听器中的事件有参数 ${arg1}${arg2}`);
});
// 第三个监听器
myEmitter.on("event", function thirdListener(...args) {
  const parameters = args.join(", ");
  console.log(`第三个监听器中的事件有参数 ${parameters}`);
});
// 第4个监听器
myEmitter.once("event", function fouthListener(...args) {
  const parameters = args.join(", ");
  console.log(`once4 ${parameters}`);
});

// 第5个监听器
myEmitter.once("event", function fifthListener(...args) {
  const parameters = args.join(", ");
  console.log(`once5 ${parameters}`);
});

myEmitter.emit("event", 1, 2, 3, 4, 5); // 第一次调用时,所有的都会执行
myEmitter.emit("event", 1, 2, 3, 4); //  第二次执行时,只执行on注册的监听器,once注册的都已经被清理了

console.log(myEmitter.listeners("event"));

如何实现先发布再订阅呢?

上面的例子中,只能实现先订阅,再发布。但是在实际项目中,如 qq、微信等离线消息。我们需要先将消息保存下来,等到有对象来订阅它们,再重新将消息发布给订阅者。

为了满足这种需求,我们就需要创建一个存放离线事件的堆栈,当事件发布时,如果还没有订阅者。则先通过函数包裹事件,等待有对象订阅时,我们将遍历队列且依次执行包装函数。且离线事件的生命周期只有一次,就像未读消息只会被重新阅读一次

下面,我们来改造上面的类

class EventEmitter {
  constructor() {
    this.listenersMap = {};
    this.emitCache = {}; // 发布事件缓存堆栈
  }

  // 监听器
  on(eventName, listener) {
    if (this.emitCache.hasOwnProperty(eventName)) {
      this.emitCache[eventName].forEach((listenerWrap) => {
        listenerWrap(listener);
      });
      delete this.emitCache[eventName];
    } else {
      this.listenersMap.hasOwnProperty(eventName)
        ? this.listenersMap[eventName].push(listener)
        : (this.listenersMap[eventName] = [listener]);
    }
    return this;
  }

  // 添加单次监听器 listener 到名为 eventName 的事件。 当 eventName 事件下次触发时,监听器会先被移除,然后再调用。
  once(eventName, listener) {
    listener._once = true;
    this.listenersMap.hasOwnProperty(eventName)
      ? this.listenersMap[eventName].push(listener)
      : (this.listenersMap[eventName] = [listener]);
    return this;
  }

  // 触发
  emit(eventName) {
    const args = [].slice.call(arguments, 1);
    if (this.listenersMap.hasOwnProperty(eventName)) {
      this.listenersMap[eventName] &&
        (this.listenersMap[eventName] = this.listenersMap[eventName].filter(
          (listener, index) => {
            listener.apply(null, args);
            return !listener._once;
          }
        ));
    } else {
      this.emitCache[eventName]
        ? this.emitCache[eventName].push(function listenerWrap(fn) {
            fn(args);
          })
        : (this.emitCache[eventName] = [
            function listenerWrap(fn) {
              fn(args);
            },
          ]);
    }
    return this;
  }

  off(eventName, fn) {
    this.listenersMap[eventName] &&
      (this.listenersMap[eventName] = this.listenersMap[eventName].filter(
        (listener, index) => {
          return listener !== fn;
        }
      ));
  }

  // listeners
  listeners(eventName) {
    return this.listenersMap[eventName];
  }
}

var myEmitter = new EventEmitter();

myEmitter.emit("event", 1, 2, 3, 4, 5); // 第一次调用时,所有的都会执行

myEmitter.on("event", function firstListener() {
  console.log("第一个监听器");
});

优点

  • 时间上的解耦,即订阅对象不需要烦恼何时去关注信息,只需要等待发布者在合适的时间发布信息并接受即可
  • 对象之间的解耦,即订阅对象和发布对象不需要强耦合在一起,当有新的对象需要接受消息时,他只需要订阅即可,发布对象无需关心订阅对象

缺点

  • 创建一个订阅对象需要时间和内存,但是可能并不会推送消息。那么这个订阅对象可能一直存在于内存中
  • 发布订阅会弱化对象与对象之间的关系,如果过度使用,对象与对象之间的必要联系都会被深埋在背后,导致程序难以理解和维护。尤其是嵌套使用发布订阅,这个时候追踪 bug 变得困难。
上一篇:
JavaScript设计模式 -- 4.迭代器模式
下一篇:
JavaScript设计模式 -- 6.命令模式
本文目录
本文目录