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

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

往期回顾

相关推荐
HEJOO913 小时前
深入理解 Java volatile 关键字:原理、用法与常见误区
java·开发语言·spring
曹牧13 小时前
Java:no content to map due to end of input
java·开发语言
子非鱼a13 小时前
【WEB】multiSQL
前端
梦梦代码精13 小时前
《回收租赁系统技术选型避坑指南:业务闭环与二开自由度详解》
开发语言·低代码·docker·开源·代码规范
隔窗听雨眠14 小时前
记一次SQL Server数据库性能分析:从CPU100%到单配置修复的完整诊断
开发语言·数据库·php
David猪大卫14 小时前
【C++修炼】智能指针使用及原理
开发语言·c++·经验分享·笔记·学习·考研·面试
charlie11451419114 小时前
现代Qt开发之图像处理进阶:像素操作与格式转换
开发语言·图像处理·qt
李永奉14 小时前
中科蓝讯SDK开发-BT893x 面条耳机连接vivo手机后连接APP端,最后手机端设置页面手动断开蓝牙连接,此时耳机端假断开
c语言·开发语言·嵌入式硬件·物联网·智能手机·电脑
lemon_sjdk14 小时前
DOM 节点
java·前端·javascript
nvd1114 小时前
深入现代 Web 鉴权架构:网关统一代理 (Forward-Auth) vs 前端持有 JWT 的终极选型与边缘同域实践
前端·架构·状态模式