观察者模式

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

主要解决的问题

  • 观察者模式解决的是一个对象状态改变时,如何自动通知其他依赖对象的问题,同时保持对象间的低耦合和高协作性。

使用场景

  • 当一个对象的状态变化需要同时更新其他对象时。

实现方式

  • 定义观察者接口:包含一个更新方法。
  • 创建具体观察者:实现观察者接口,定义接收到通知时的行为。
  • 定义主题接口:包含添加、删除和通知观察者的方法。
  • 创建具体主题:实现主题接口,管理观察者列表,并在状态改变时通知它们。

关键代码

  • 观察者列表:在主题中维护一个观察者列表。

应用实例

气象站(被观察者)监测天气变化,当温度、湿度等数据变化时,自动通知所有注册的显示设备(观察者),如手机APP、电子公告牌、机场大屏等。

优点

  • 抽象耦合:观察者和主题之间是抽象耦合的。
  • 触发机制:建立了一套状态改变时的触发和通知机制。

缺点

  • 性能问题:如果观察者众多,通知过程可能耗时。
  • 循环依赖:可能导致循环调用和系统崩溃。
  • 缺乏变化详情:观察者不知道主题如何变化,只知道变化发生。

注意事项

  • 避免循环引用:注意观察者和主题之间的依赖关系,避免循环引用。
  • 异步执行:考虑使用异步通知避免单点故障导致整个系统卡壳。

结构

观察者模式包含以下几个核心角色:

  • 主题(Subject):也称为被观察者或可观察者,它是具有状态的对象,并维护着一个观察者列表。主题提供了添加、删除和通知观察者的方法。
  • 观察者(Observer):观察者是接收主题通知的对象。观察者需要实现一个更新方法,当收到主题的通知时,调用该方法进行更新操作。
  • 具体主题(Concrete Subject):具体主题是主题的具体实现类。它维护着观察者列表,并在状态发生改变时通知观察者。
  • 具体观察者(Concrete Observer):具体观察者是观察者的具体实现类。它实现了更新方法,定义了在收到主题通知时需要执行的具体操作。

观察者模式通过将主题和观察者解耦,实现了对象之间的松耦合。当主题的状态发生改变时,所有依赖于它的观察者都会收到通知并进行相应的更新。

观察者列表:用来存储每一个被观察者被谁观察了,如果某个被观察者有数据更新,就自动通知列表中对应的观察者

耦合:耦合是指代码中模块/类/组件之间相互依赖的程度

  • 主题只依赖 Observer接口(抽象),不关心具体是哪个观察者。
  • 观察者只依赖 Subject接口(抽象),不关心具体是哪个主题。
// 主题只认Observer接口(松耦合)
被观察者
interface Subject {
    void addObserver(Observer o); // 参数是抽象接口
    
}
观察者
// 观察者只认Subject接口(松耦合)
interface Observer {
    void update(String message); // 不依赖具体主题
}

举例

(1) DOM 事件监听
// 被观察者:按钮
const button = document.querySelector('button');

// 观察者:点击事件回调
button.addEventListener('click', () => {
    console.log('按钮被点击了!');
});
  • button 是被观察者,addEventListener就是 addObserver
  • 回调函数 就是观察者,当按钮被点击时自动触发。
(2) 响应式数据
  • Vue 2(Object.defineProperty

    const data = { count: 0 };
    
    // 观察者:Watcher
    new Vue({
        data: data,
        watch: {
            count(newVal) {
                console.log('count 变了:', newVal);
            }
        }
    });
    
    data.count = 1; // 触发 watch 回调
    
  • Vue 3(Proxy

    const state = reactive({ count: 0 });
    
    // 观察者:effect
    effect(() => {
        console.log('count:', state.count);
    });
    
    state.count++; // 自动触发 effect
    
(3) Redux 的状态订阅
const store = createStore(reducer);

// 观察者:订阅 store 变化
store.subscribe(() => {
    console.log('状态更新了:', store.getState());
});

store.dispatch({ type: 'INCREMENT' }); // 触发订阅回调

发布订阅者模式

发布-订阅模式是一种消息范式,消息的发送者(发布者)不会直接将消息发送给特定的接收者(订阅者),而是通过消息代理(事件通道)进行广播,订阅者可以接收感兴趣的消息。

解决问题

通过消息中介(事件总线/消息队列)彻底解耦发布者和订阅者,让互不知情的双方能安全通信。

应用实例

外卖订单状态更新
  • 发布者:商家/骑手
  • 订阅者:顾客
  • 消息代理:美团/饿了么系统
  • 过程:订单状态变更(接单、配送中、送达)→ 推送给顾客

优点:

  1. 完全解耦
    • 发布者和订阅者互不知晓对方存在,通过消息代理(Broker)通信,系统更灵活。
  2. 动态扩展性强
    • 新增订阅者无需修改发布者代码,只需订阅对应频道。
    • 示例:在日志系统中新增一个分析服务,只需订阅日志流,不影响原有服务。
  3. 支持多对多通信
    • 一个消息可被多个订阅者消费,一个订阅者可监听多个频道。
    • 示例:股票价格变动同时推送给APP、短信服务和数据库。
  4. 异步处理能力
    • 发布者发完消息即可返回,订阅者按自身节奏处理,避免阻塞。
    • 示例:用户注册后发欢迎邮件,邮件服务可异步处理,不拖慢注册流程。
  5. 故障隔离
    • 单个订阅者崩溃不影响整体系统(消息代理可重试或持久化消息)。
    • 示例:支付成功消息存到RabbitMQ,即使积分服务暂时宕机,恢复后仍能处理。

缺点:

  1. 系统复杂度增加

    • 需额外维护消息代理(如Redis/Kafka),可能引入新故障点。
    • 问题:消息代理宕机会导致整个系统不可用。
  2. 调试困难

    • 消息流经多个组件,问题追踪链条长(需分布式日志追踪)。
    • 示例:订单丢失时,需检查发布者、消息队列、订阅者三处日志。
  3. 消息顺序难保证

    • 分布式场景下,消息可能乱序到达(需业务层处理幂等性)。
    • 示例:订单状态"已付款" → "已发货"的消息若颠倒,会导致逻辑错误。
  4. 性能瓶颈风险

    • 高频消息可能压垮消息代理(如每秒百万级消息需Kafka集群支撑)。
    • 示例:双11秒杀活动若直接用Redis Pub/Sub,可能因流量过大崩溃。
  5. 消息堆积问题

    • 若订阅者处理速度慢,消息代理可能积压大量未消费消息。
    • 示例:促销期间用户注册消息激增,邮件服务处理不过来导致队列阻塞。

结构:

1. 发布者(Publisher)
  • 职责:产生消息并发送到指定频道(Channel)。

  • 特点

    • 不关心谁接收消息。
    • 不直接与订阅者交互。
  • 示例代码

    // 发布订单创建事件
    eventBus.publish('order.created', { orderId: 123 });
    
2. 订阅者(Subscriber)
  • 职责:订阅感兴趣的频道,接收并处理消息。

  • 特点

    • 不关心消息来源。
    • 可动态订阅/取消订阅。
  • 示例代码

    // 订阅订单事件
    eventBus.subscribe('order.created', (order) => {
      console.log('库存系统收到订单:', order);
    });
    
3. 消息代理(Broker / Event Bus)
  • 职责
    • 管理频道(Channel)的注册和消息路由。
    • 存储并转发消息(可能支持持久化)。
  • 关键功能
    • 频道管理:维护 频道 → 订阅者列表的映射。
    • 消息过滤:按规则分发消息(如主题匹配、标签过滤)。
    • 可靠性:支持ACK确认、重试、死信队列等。
  • 典型实现
    • Redis Pub/Sub
    • Kafka
    • RabbitMQ
    • MQTT Broker(如EMQX)
4. 频道(Channel / Topic)
  • 职责:消息的分类标识,用于定向路由。
  • 特点
    • 发布者和订阅者通过频道解耦。
    • 可支持通配符(如 news.*)。
  • 示例
    • order.created(订单创建)
    • user.login(用户登录)

[发布者] ─── “order.created” ───▶ [消息代理] ────▶ [订阅者1(库存系统)]

​ │ │ |

​ └── “user.login” ────────┘ └──────▶ [订阅者2(日志系统)]

所有频道共享一个消息代理

  • 资源复用:单个代理可管理多个频道,减少系统复杂度。

  • 统一控制:集中处理消息持久化、重试、安全策略等。

  • 性能优化:代理集群可横向扩展,无需为每个频道单独部署

    上面是标准的发布订阅者实现模式,但是有例外

    在超大规模系统中可能拆分代理,例如:

    • 按业务隔离:订单类消息用 Broker A,用户类消息用 Broker B。
    • 按地域隔离:不同数据中心部署独立代理。

    但这类设计会引入额外复杂度,需配套服务发现和负载均衡机制。

举例:

(1) Vue/React 事件总线

// Vue EventBus(全局事件中心)
const eventBus = new Vue();
eventBus.$emit('user-login', userData);  // 发布
eventBus.$on('user-login', callback);    // 订阅

用途:跨组件通信(如A组件通知B组件用户已登录)。

(2) Redux / Vuex 状态管理

// Redux 订阅 store 变化
store.subscribe(() => {
  console.log('状态更新:', store.getState());
});

用途:状态变更时通知所有订阅的组件。

(3) WebSocket 实时通信

socket.on('new-message', (msg) => {  // 订阅消息
  console.log('收到消息:', msg);
});
socket.emit('send-message', text);   // 发布消息

用途:聊天室、实时协作编辑(如Google Docs)。

对比

对比维度 观察者模式 (Observer Pattern) 发布-订阅模式 (Pub-Sub Pattern)
耦合度 较高(被观察者直接持有观察者引用) 完全解耦(通过消息代理中介)
通信方式 被观察者直接调用观察者的方法(如 observer.update() 发布者和订阅者通过消息代理通信(如 eventBus.emit()
关系模型 一对多(一个被观察者 → 多个观察者) 多对多(多个发布者 → 多个订阅者 + 多个频道)
中间层 必须存在消息代理(Broker/Event Bus)
典型实现 DOM事件、Vue的Watcher、Redux的store.subscribe() Kafka、Redis Pub/Sub、Vue EventBus
代码示例 button.addEventListener('click', callback); eventBus.emit('message', data);
动态扩展 需修改被观察者代码来增删观察者 可动态增删订阅者,无需修改发布者
性能影响 高频通知可能阻塞主线程 支持异步处理,适合高并发场景
适用场景 简单的一对多通知(如UI组件联动) 分布式系统、跨模块通信、微服务架构
调试难度 较易(调用链路直接) 较难(需追踪消息代理的路由逻辑)

在前端开发中,大多数情况下不需要手写发布-订阅或观察者模式,主流框架和浏览器API已内置这些模式的实现。

需要手写的场景(特殊情况)

(1) 无框架的纯原生项目

例如用纯JavaScript开发小型工具库时,可能需要简易实现:

// 手写发布-订阅
class EventBus {
  constructor() {
    this.events = {};
  }
  on(event, callback) {
    this.events[event] = this.events[event] || [];
    this.events[event].push(callback);
  }
  emit(event, data) {
    this.events[event]?.forEach(cb => cb(data));
  }
}
(2) 定制高级功能
  • 需要消息持久化:结合IndexedDB存储事件。
  • 复杂路由逻辑:如按消息标签过滤(user:login:success)。
  • 性能优化:防抖/节流、批量通知等。
(3) 教学/面试场景

手写简易实现有助于理解设计模式原理(但实际项目仍用现成方案)。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐