发布订阅模式 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);
在这个例子中:
fetchData
完成后发布data-ready
事件renderUI
订阅了该事件,在事件触发时执行- 两个函数完全不知道对方的存在,通过事件中心解耦
发布订阅的三大优势
- 完全解耦:发布者和订阅者互不知晓对方
- 动态管理:可随时添加/移除订阅者
- 多对多关系:一个事件可以有多个订阅者,一个订阅者可关注多个事件
观察者模式:直接通知的”点名系统”
生活中的类比
想象一个班主任在教室里宣布通知。老师清楚地知道班上每个学生,通知时会直接看向每个学生。这里,老师(主题)和学生(观察者)是直接关联的。
技术实现
让我们通过一个实际的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直接调用所有依赖该数据的观察者(如视图渲染函数),不需要中间的事件中心。
两种模式的核心差异
特征 | 发布订阅模式 | 观察者模式 |
---|---|---|
通信方式 | 通过事件中心间接通信 | 主题直接通知观察者 |
耦合度 | 完全解耦 | 主题需维护观察者列表 |
关系 | 多对多 | 一对多 |
灵活性 | 高(支持复杂事件处理) | 中(适合简单通知) |
复杂度 | 较高(需实现事件中心) | 较低(直接维护列表) |
典型应用 | 全局事件总线、模块间通信 | 数据绑定、响应式系统 |
如何选择?实际场景分析
何时选择发布订阅模式
-
跨组件通信:在React应用中,使用事件总线实现非父子组件通信
1eventBus.emit('user-logged-in', userData); 2 3 4eventBus.on('user-logged-in', (user) => { 5 6});
-
微服务架构:不同服务通过消息队列(事件中心)通信
-
插件系统:核心系统发布事件,插件订阅感兴趣的事件
何时选择观察者模式
-
数据绑定:如Vue的响应式系统
1new Vue({ 2 data: { message: 'Hello' }, 3 watch: { 4 message(newVal) { 5 console.log('消息变化了:', newVal); 6 } 7 } 8})
-
状态管理:Redux中的store通知所有订阅的组件
-
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实例作为事件中心通信。
总结:根据场景选择最佳方案
理解两种模式的核心区别后,我们可以得出以下实践建议:
- 当你需要完全解耦的组件通信 → 选择发布订阅
- 当你处理明确的主从关系 → 选择观察者模式
- 在性能敏感的简单场景 → 观察者模式更高效
- 需要复杂事件处理时 → 发布订阅更灵活
发布订阅和观察者模式就像工具箱中的不同工具,没有绝对的优劣,只有适合的场景。真正优秀的开发者不仅会使用这些模式,更能理解其背后的设计哲学,根据实际需求灵活变通。
下次当你在代码中实现事件通信时,不妨先问自己:我的组件之间是像公众号和订阅者(发布订阅),还是像老师和学生(观察者)?这个简单的思考将帮助你选择最合适的设计模式。