【发布订阅】从WeakMap和Map来看前端发布订阅者模式

Map和WeakMap都是用于存储键值对的数据结构,但是也有一定的区别:

  1. Map的键可以是任意类型的数据,但WeakMap只接受对象作为键(null除外),不接受其他类型的值作为键
  2. Map可遍历,WeakMap不可遍历

Map的优势在频繁增删键值对的场景下,Map的性能优于Object

WeakMap的键是弱引用,键所指向的对象可以被垃圾回收(GC)回收。当对象的其他引用都被清除时,WeakMap中的键值对会自动消失,不需要手动删除引用

再来看一个完整的发布订阅模式(最后其实还包含了单例的思想):
参考地址
https://juejin.cn/post/7464570913678164007

export class SubscriptionPublish{
  constructor() {
    // 使用Map存储事件队列(比Object更高效)
    this.eventMap = new Map();
    // 用于once的WeakMap(防止内存泄漏)
    this.onceWrapperMap = new WeakMap();
  }

  on(eventName, handler) {
    if (typeof handler !== 'function') {
        throw new TypeError('Handler must be a function');
    }
    
    const handlers = this.eventMap.get(eventName) || [];
    handlers.push(handler);
    this.eventMap.set(eventName, handlers);
  }

  emit(eventName, ...args) {
    const handlers = this.eventMap.get(eventName);
    if (!handlers) return false;

    // 创建副本执行(防止执行过程中修改队列)
    handlers.slice().forEach(handler => {
        // 异步执行更贴近实际场景(面试加分点)
        Promise.resolve().then(() => {
            handler.apply(this, args);
        });
    });
    return true;
  }

 off(eventName, handler) {
    const handlers = this.eventMap.get(eventName);
    if (!handlers) return;

    // 双保险删除(直接删除+通过once包装删除)
    const index = handlers.findIndex(
      h => h === handler || h === this.onceWrapperMap.get(handler)
    );
    
    if (index > -1) {
      handlers.splice(index, 1);
      // 清理空数组
      if (handlers.length === 0) {
        this.eventMap.delete(eventName);
      }
    }
 }

 once(eventName, handler) {
    const onceHandler = (...args) => {
      // 先执行再清理(避免中途报错导致未清理)
      try {
        handler.apply(this, args);
      } finally {
        this.off(eventName, onceHandler);
        this.onceWrapperMap.delete(handler);
      }
    };

    // 建立原始handler与包装后的映射
    this.onceWrapperMap.set(handler, onceHandler);
    this.on(eventName, onceHandler);
 }
}

const defaultEvent = new SubscriptionPublish();
export default defaultEvent;
// 使用方式
import defaultEvent from './subscription'

defaultEvent.on('input', function input(data) {
  console.log('输入'+data);
});

defaultEvent.on('output', function output(data) {
  console.log('输出'+data);
});

console.log(defaultEvent);

defaultEvent.emit('input', 1)  // 输入1
defaultEvent.emit('output',2)  // 输出2
  1. constructor中定义了事件存储的两个map,分别是常规事件和一次性事件。
  2. on先判断handler类型满足函数,其次取出对应的事件数组,并将事件塞入,以备订阅。
  3. emit订阅,先判断订阅是否存在,其次slice返回新数组,将当前调用位置的this传入,并循环调用内部handler。
  4. off取消订阅,首先判断是否存在,其次进行删除处理
  5. once返回一个onceHandler,try的finally中,确保函数被调用一次肯定会被销毁,如果没有保存,handler.apply(this, args);会返回一次方法,并供调用。

上面的call和apply可以查看以前的文章。

你可能感兴趣的:(前端)