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

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

往期回顾

相关推荐
IT策士10 小时前
第45篇 k8s之实战:将 Web 应用迁移到 Kubernetes(下)
前端·容器·kubernetes
知识的宝藏10 小时前
Xpaht self::div 轴语法
开发语言
云水一下10 小时前
TypeScript 从零基础到精通(二):基础类型与类型系统
javascript·typescript
keykey6.10 小时前
卷积神经网络(CNN):让AI学会“看“
开发语言·人工智能·深度学习·机器学习
IsJunJianXin10 小时前
谷歌搜索cookie NID逆向生成
开发语言·python·google搜索·sgss·nid-cookie·算法生成nid·google-cookie
weikecms10 小时前
美团霸王餐报名API接口
java·开发语言
你怎么知道我是队长10 小时前
CRC校验C语言实现-CRC8、CRC16、CRC16的直接计算法、查表法
c语言·前端·javascript
繁星蓝雨10 小时前
C++中对比pragma once和ifndef的使用区别
开发语言·c++·ifndef·头文件·pragma once
.千余10 小时前
【C++】C++手写Vector容器:从底层源码模拟实现
开发语言·c++·经验分享·笔记·学习
Rain50911 小时前
mini-cc 终端 UI:用 React 写 CLI 是什么体验
前端·人工智能·react.js·ui·架构·前端框架·ai编程