发布订阅模式 vs 观察者模式

发布订阅模式 vs 观察者模式:它们真的是一回事吗?

你在学习前端开发时是否也曾困惑:发布订阅和观察者模式听起来如此相似,它们究竟有什么区别?为什么有些资料说它们是一回事,而实际代码实现却截然不同?今天,我们将揭开这两种模式的神秘面纱。

为什么我们需要设计模式?

在深入探讨之前,让我们思考一个问题:为什么我们需要这些设计模式? 想象你正在开发一个复杂的前端应用,多个组件需要相互通信,但又不能紧密耦合在一起。就像城市中的交通系统,如果每辆车都直接与其他车辆通信,那将是灾难性的混乱。设计模式就是为此而生的通信规则手册,让我们的代码保持清晰、可维护和可扩展。

发布订阅模式:事件驱动的消息中心

生活中的类比

想象你订阅了一个技术博客的邮件列表。当博客发布新文章时,所有订阅者都会收到邮件通知。有趣的是,博客作者并不知道具体有哪些订阅者,他们只需要将文章发布到平台上,平台负责通知所有人。

发布订阅模式正是基于这样的思想:发布者和订阅者之间通过一个事件中心进行通信,彼此不直接接触

让我们看一个具体的代码实现:

 1class EventEmitter {
 2    constructor() {
 3        this.eventList = {}
 4    }
 5
 6    
 7    on(eventName, callBack) {
 8        if (!this.eventList[eventName]) {
 9            this.eventList[eventName] = [];
10        }
11        this.eventList[eventName].push(callBack)
12    }
13
14    
15    emit(eventName) {
16        if (this.eventList[eventName]) {
17            const callBacks = this.eventList[eventName].slice()
18            callBacks.forEach((item) => {
19                item()
20            })
21        }
22    }
23
24    
25    off(eventName, callBack) {
26        if (this.eventList[eventName]) {
27            this.eventList[eventName] = this.eventList[eventName].filter((item) => {
28                return item !== callBack
29            })
30        }
31    }
32
33    
34    once(eventName, callBack) {
35        let onceCallBack = () => {
36            callBack()
37            this.off(eventName, onceCallBack)
38        }
39        this.on(eventName, onceCallBack)
40    }
41}

这个 EventEmitter 类就是我们的”事件中心”。它维护着一个 eventList 对象,用来存储所有的事件和对应的回调函数。当我们调用 on 方法时,就是在订阅某个事件;调用 emit 方法时,就是在发布事件。

发布订阅模式的实际应用

 1function fetchData() {
 2    setTimeout(() => {
 3        console.log('数据加载完成');
 4        _event.emit('data-ready');  
 5    }, 1000)
 6}
 7
 8function renderUI() {
 9    setTimeout(() => {
10        console.log('UI渲染完成');
11    }, 500)
12}
13
14
15const _event = new EventEmitter();
16
17fetchData();
18_event.on('data-ready', renderUI);  

在这个例子中:

  1. fetchData 完成后发布 data-ready 事件
  2. renderUI 订阅了该事件,在事件触发时执行
  3. 两个函数完全不知道对方的存在,通过事件中心解耦

发布订阅的三大优势

  1. 完全解耦:发布者和订阅者互不知晓对方
  2. 动态管理:可随时添加/移除订阅者
  3. 多对多关系:一个事件可以有多个订阅者,一个订阅者可关注多个事件

观察者模式:直接通知的”点名系统”

生活中的类比

想象一个班主任在教室里宣布通知。老师清楚地知道班上每个学生,通知时会直接看向每个学生。这里,老师(主题)和学生(观察者)是直接关联的。

技术实现

让我们通过一个实际的DOM操作例子来理解观察者模式:

 1<script>
 2    let h2 = document.querySelector('h2')
 3    let btn = document.querySelector('button')
 4    let obj = {
 5        count: 1
 6    }
 7    let num = obj.count 
 8
 9    function observer() {
10        
11        
12        h2.innerHTML = num
13    }
14
15    Object.defineProperty(obj, 'count', {
16        get() {
17            return num
18        },
19        set(newValue) {
20            num = newValue
21            observer()  
22        }
23    })
24
25    btn.addEventListener('click', () => {
26        obj.count++
27    })
28</script>

在这个例子中,我们使用了 Object.defineProperty 来监听 count 属性的变化。当 count 发生改变时,setter 函数会直接调用 observer 函数来更新页面显示。这就是典型的观察者模式:主题(obj.count)直接通知观察者(observer函数)。

Vue响应式系统的秘密

观察者模式是Vue响应式系统的核心:

 1const data = { count: 1 };
 2
 3Object.defineProperty(data, 'count', {
 4    get() {
 5        return this._count;
 6    },
 7    set(newValue) {
 8        this._count = newValue;
 9        updateView();  
10    }
11});
12
13function updateView() {
14    console.log('视图更新了!');
15}
16
17data.count = 5; 

关键点:当数据变化时,Vue直接调用所有依赖该数据的观察者(如视图渲染函数),不需要中间的事件中心。

两种模式的核心差异

特征发布订阅模式观察者模式
通信方式通过事件中心间接通信主题直接通知观察者
耦合度完全解耦主题需维护观察者列表
关系多对多一对多
灵活性高(支持复杂事件处理)中(适合简单通知)
复杂度较高(需实现事件中心)较低(直接维护列表)
典型应用全局事件总线、模块间通信数据绑定、响应式系统

如何选择?实际场景分析

何时选择发布订阅模式

  1. 跨组件通信:在React应用中,使用事件总线实现非父子组件通信

     1eventBus.emit('user-logged-in', userData);
     2
     3
     4eventBus.on('user-logged-in', (user) => {
     5  
     6});
    
  2. 微服务架构:不同服务通过消息队列(事件中心)通信

  3. 插件系统:核心系统发布事件,插件订阅感兴趣的事件

何时选择观察者模式

  1. 数据绑定:如Vue的响应式系统

     1new Vue({
     2  data: { message: 'Hello' },
     3  watch: {
     4    message(newVal) { 
     5      console.log('消息变化了:', newVal);
     6    }
     7  }
     8})
    
  2. 状态管理:Redux中的store通知所有订阅的组件

  3. DOM事件:浏览器内置的事件系统

     1button.addEventListener('click', handler); 
    

性能与复杂度权衡

发布订阅模式在大型系统中优势明显:

  • 支持更复杂的事件处理(过滤、转换、优先级)
  • 完全解耦使系统更易扩展
  • 但引入中间层带来轻微性能开销

观察者模式在简单场景更高效:

  • 直接通知减少中间环节
  • 实现简单明了
  • 但当观察者数量巨大时,直接遍历列表可能成为性能瓶颈

常见误区澄清

误区1:“浏览器事件是发布订阅”

❌ 实际上,浏览器的addEventListener观察者模式的实现:

  • DOM元素(主题)直接维护监听器列表
  • 事件触发时直接调用所有监听器

误区2:“Vue的EventBus是观察者模式”

❌ 实际上,Vue的EventBus是发布订阅的典型应用:

 1const EventBus = new Vue();
 2
 3
 4EventBus.$emit('data-updated', payload);
 5
 6
 7EventBus.$on('data-updated', handleData);

这里没有直接依赖,通过Vue实例作为事件中心通信。

总结:根据场景选择最佳方案

理解两种模式的核心区别后,我们可以得出以下实践建议:

  1. 当你需要完全解耦的组件通信 → 选择发布订阅
  2. 当你处理明确的主从关系 → 选择观察者模式
  3. 性能敏感的简单场景 → 观察者模式更高效
  4. 需要复杂事件处理时 → 发布订阅更灵活

发布订阅和观察者模式就像工具箱中的不同工具,没有绝对的优劣,只有适合的场景。真正优秀的开发者不仅会使用这些模式,更能理解其背后的设计哲学,根据实际需求灵活变通。

下次当你在代码中实现事件通信时,不妨先问自己:我的组件之间是像公众号和订阅者(发布订阅),还是像老师和学生(观察者)?这个简单的思考将帮助你选择最合适的设计模式。

个人笔记记录 2021 ~ 2025