JavaScript设计模式(四):发布-订阅模式实现与应用

1、定义

发布-订阅模式描述的是一种解耦 的协作方式:发布者 (发布消息的人/系统)不需要知道谁会接收消息,订阅者 (接收消息的人/系统)也不需要知道消息从哪来,双方通过一个中间层(消息通道/代理)来连接。

它里面包含三个角色:

  • 发布者 publisher:负责"发消息"。
  • 订阅者 subscriber:负责"收消息"。
  • 事件中心 event bus:负责保存订阅关系,并在事件发生时通知所有订阅者。

其核心思路就是:发布者发消息,订阅者收消息,中间通过一个事件中心进行统一管理

2、生活中的例子

发布-订阅模式在生活中也有广泛的应用,比如社交媒体的"关注"机制,手机上的 App 推送通知,群聊中的"@所有人"等。

你关注了几个公众号,只要它们发文章,你就会收到消息:

  • 公众号相当于发布者。
  • 你相当于订阅者。
  • 微信平台相当于中间的消息中心。

3、发布-订阅模式实现

它的核心功能如下:

  • subscribe:订阅事件。
  • publish:发布事件。
  • unsubscribe:取消订阅。
  • events: 事件中心对象,用来保存不同事件对应的回调函数。

具体实现代码如下

js 复制代码
class EventBus {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  subscribe(eventName, handler) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }

    this.events[eventName].push(handler);
  }

  // 发布事件
  publish(eventName, data) {
    const handlers = this.events[eventName];

    if (!handlers || handlers.length === 0) {
      return;
    }

    handlers.forEach(handler => {
      handler(data);
    });
  }

  // 取消订阅
  unsubscribe(eventName, handler) {
    const handlers = this.events[eventName];

    if (!handlers) {
      return;
    }

    this.events[eventName] = handlers.filter(item => item !== handler);
  }
}

使用代码如下

js 复制代码
const bus = new EventBus();

function handleLogin(data) {
  console.log('更新用户信息:', data);
}

function handleCart(data) {
  console.log('同步购物车数据:', data);
}

// 订阅 login 事件
bus.subscribe('login', handleLogin);
bus.subscribe('login', handleCart);

// 发布 login 事件
bus.publish('login', { username: 'xiaoming' });

/**
 * 输出结果:
 * 
 * 更新用户信息: { username: 'xiaoming' }
 * 同步购物车数据: { username: 'xiaoming' }
 */

也就是说,当 login 事件被发布后,所有订阅了这个事件的函数都会收到通知并执行,如果某个模块不想在监听这个事件了,可以通过 unsubscribe 取消订阅。

js 复制代码
bus.unsubscribe('login', handleCart); // 取消订阅 login 事件的 handleCart 回调
bus.publish('login', { username: 'xiaoming' });

/**
 * 输出结果:
 * 
 * 更新用户信息: { username: 'xiaoming' }
 */

为了更加实用,我们可以在 subscribe 方法的返回值中直接返回一个"取消订阅函数",这样用起来会更方便。

js 复制代码
class EventBus {
  // ...
  // 订阅事件
  subscribe(eventName, handler) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }

    this.events[eventName].push(handler);
    // 增加返回回调
    return () => {
      this.unsubscribe(eventName, handler);
    };
  }
  // ...
}

使用方式:

js 复制代码
const bus = new EventBus();

const cancel = bus.subscribe('message', data => {
  console.log('收到消息:', data);
});

bus.publish('message', 'Hello');
cancel();
bus.publish('message', 'World');

/**
 * 输出结果:
 * 
 * 第一次输出:收到消息: Hello
 * 第二次不会输出,因为订阅已经被取消了。
 */

当你"注册"了某种副作用,最好顺手拿到一个"撤销它"的能力 。这种思想在不同框架和库里非常常见,比如 React 里的 cleanupVue3 中的 stop

js 复制代码
// react
useEffect(() => {
  const handler = () => {
    console.log('resize');
  };

  window.addEventListener('resize', handler);

  return () => {
    window.removeEventListener('resize', handler);
  };
}, []);
js 复制代码
// vue3
const stop = watch(source, (newValue) => {
  console.log(newValue);
});

stop(); // 取消 watch 监听

4、发布-订阅模式的优缺点

4.1 优点:

  • 解耦性:发布者和订阅者彼此独立,容易维护和扩展。
  • 异步通信:组件通信更灵活,发布者和订阅者不受时间限制,可以在任意时间发布和订阅事件。

4.2 缺点:

  • 难以调试和追踪:事件太多时,系统中存在大量发布者和订阅者,会导致系统复杂度上升,代码变乱,程序难以跟踪维护和理解。
  • 内存泄漏:忘记取消订阅可能造成内存泄漏。

5、发布-订阅模式的应用

发布-订阅模式在下列场景有广泛的应用:

  • 组件之间通信。
  • 全局消息通知。
  • 自定义事件系统。
  • 状态变化通知。
  • 页面埋点、日志上报。

6、和观察者模式的区别

发布-订阅模式和观察者模式很容易混淆。

  • 观察者模式:观察者直接订阅目标对象,关系更直接。
  • 发布-订阅模式:中间多了一个"事件中心",发布者和订阅者互相不知道对方。

所以发布-订阅模式的解耦更强。

小结

上面介绍了Javascript最经典的设计模式之一发布-订阅模式,发布-订阅描述的是一种解耦的协作方式,发布者发送消息,订阅者接收消息,中间层(事件中心)负责保存订阅关系,并在事件发生时通知所有订阅者。

它让发布者和订阅者之间解藕,可以很方便的实现异步通信,但如果过度使用,也会让系统难以调试和追踪,忘记取消订阅可能造成内存泄漏,在实际项目中可根据需要使用。

往期回顾

相关推荐
27669582921 小时前
租车帮(悟空)订单查询算法分析
java·服务器·前端·悟空·悟空app·租车帮·租车帮逆向
用户326658403742 小时前
如何初始化 TypeScript + Node.js 项目
javascript
__雨夜星辰__2 小时前
TypeScript 入门学习笔记(面向对象 + 常用设计模式)
前端·学习·typescript
晚霞的不甘2 小时前
HarmonyOS ArkTS 进阶实战:深入理解边距、边框与嵌套布局
前端·计算机视觉·华为·智能手机·harmonyos
_野猪佩奇_牛马版2 小时前
ReACT Agent 开发知识点总结
前端
bmseven2 小时前
23种设计模式 - 工厂方法(Factory Method)
设计模式·工厂方法模式
牛奶2 小时前
你发送的消息,微信到底怎么送到的?
前端·websocket·http
pqq的迷弟2 小时前
设计模式的原则
设计模式
酉鬼女又兒2 小时前
零基础快速入门前端DOM 元素获取方法详解:从代码到实践(可用于备赛蓝桥杯Web应用开发)
前端·javascript·职场和发展·蓝桥杯·js