发布订阅模式 vs 观察者模式:它们真的是一回事吗?
你在学习前端开发时是否也曾困惑:发布订阅和观察者模式听起来如此相似,它们究竟有什么区别?为什么有些资料说它们是一回事,而实际代码实现却截然不同?今天,我们将揭开这两种模式的神秘面纱。
为什么我们需要设计模式?
在深入探讨之前,让我们思考一个问题:为什么我们需要这些设计模式? 想象你正在开发一个复杂的前端应用,多个组件需要相互通信,但又不能紧密耦合在一起。就像城市中的交通系统,如果每辆车都直接与其他车辆通信,那将是灾难性的混乱。设计模式就是为此而生的通信规则手册,让我们的代码保持清晰、可维护和可扩展。
发布订阅模式:事件驱动的消息中心
生活中的类比
想象你订阅了一个技术博客的邮件列表。当博客发布新文章时,所有订阅者都会收到邮件通知。有趣的是,博客作者并不知道具体有哪些订阅者,他们只需要将文章发布到平台上,平台负责通知所有人。
发布订阅模式正是基于这样的思想:发布者和订阅者之间通过一个事件中心进行通信,彼此不直接接触。
让我们看一个具体的代码实现:
javascript
class EventEmitter {
constructor() {
this.eventList = {}
}
// 订阅事件
on(eventName, callBack) {
if (!this.eventList[eventName]) {
this.eventList[eventName] = [];
}
this.eventList[eventName].push(callBack)
}
// 发布事件
emit(eventName) {
if (this.eventList[eventName]) {
const callBacks = this.eventList[eventName].slice()
callBacks.forEach((item) => {
item()
})
}
}
// 取消订阅
off(eventName, callBack) {
if (this.eventList[eventName]) {
this.eventList[eventName] = this.eventList[eventName].filter((item) => {
return item !== callBack
})
}
}
// 只订阅一次
once(eventName, callBack) {
let onceCallBack = () => {
callBack()
this.off(eventName, onceCallBack)
}
this.on(eventName, onceCallBack)
}
}
这个 EventEmitter
类就是我们的"事件中心"。它维护着一个 eventList
对象,用来存储所有的事件和对应的回调函数。当我们调用 on
方法时,就是在订阅某个事件;调用 emit
方法时,就是在发布事件。
发布订阅模式的实际应用
javascript
function fetchData() {
setTimeout(() => {
console.log('数据加载完成');
_event.emit('data-ready'); // 发布事件
}, 1000)
}
function renderUI() {
setTimeout(() => {
console.log('UI渲染完成');
}, 500)
}
// 创建事件中心
const _event = new EventEmitter();
fetchData();
_event.on('data-ready', renderUI); // 订阅事件
在这个例子中:
fetchData
完成后发布data-ready
事件renderUI
订阅了该事件,在事件触发时执行- 两个函数完全不知道对方的存在,通过事件中心解耦
发布订阅的三大优势
- 完全解耦:发布者和订阅者互不知晓对方
- 动态管理:可随时添加/移除订阅者
- 多对多关系:一个事件可以有多个订阅者,一个订阅者可关注多个事件
观察者模式:直接通知的"点名系统"
生活中的类比
想象一个班主任在教室里宣布通知。老师清楚地知道班上每个学生,通知时会直接看向每个学生。这里,老师(主题)和学生(观察者)是直接关联的。
技术实现
让我们通过一个实际的DOM操作例子来理解观察者模式:
html
<script>
let h2 = document.querySelector('h2')
let btn = document.querySelector('button')
let obj = {
count: 1
}
let num = obj.count // 防止递归爆栈
function observer() {
// 找出页面上所有用到了 count 这个变量的 dom 结构 -- 找到订阅者
// 将订阅者的内容全部替换为最新的 count 变量
h2.innerHTML = num
}
Object.defineProperty(obj, 'count', {
get() {
return num
},
set(newValue) {
num = newValue
observer() // 直接调用观察者函数
}
})
btn.addEventListener('click', () => {
obj.count++
})
</script>
在这个例子中,我们使用了 Object.defineProperty
来监听 count
属性的变化。当 count
发生改变时,setter 函数会直接调用 observer
函数来更新页面显示。这就是典型的观察者模式:主题(obj.count)直接通知观察者(observer函数)。
Vue响应式系统的秘密
观察者模式是Vue响应式系统的核心:
javascript
const data = { count: 1 };
Object.defineProperty(data, 'count', {
get() {
return this._count;
},
set(newValue) {
this._count = newValue;
updateView(); // 直接调用观察者更新视图
}
});
function updateView() {
console.log('视图更新了!');
}
data.count = 5; // 触发setter,自动更新视图
关键点 :当数据变化时,Vue直接调用所有依赖该数据的观察者(如视图渲染函数),不需要中间的事件中心。
两种模式的核心差异
特征 | 发布订阅模式 | 观察者模式 |
---|---|---|
通信方式 | 通过事件中心间接通信 | 主题直接通知观察者 |
耦合度 | 完全解耦 | 主题需维护观察者列表 |
关系 | 多对多 | 一对多 |
灵活性 | 高(支持复杂事件处理) | 中(适合简单通知) |
复杂度 | 较高(需实现事件中心) | 较低(直接维护列表) |
典型应用 | 全局事件总线、模块间通信 | 数据绑定、响应式系统 |
如何选择?实际场景分析
何时选择发布订阅模式
-
跨组件通信:在React应用中,使用事件总线实现非父子组件通信
javascript// 组件A eventBus.emit('user-logged-in', userData); // 组件B eventBus.on('user-logged-in', (user) => { // 更新用户信息 });
-
微服务架构:不同服务通过消息队列(事件中心)通信
-
插件系统:核心系统发布事件,插件订阅感兴趣的事件
何时选择观察者模式
-
数据绑定:如Vue的响应式系统
javascriptnew Vue({ data: { message: 'Hello' }, watch: { message(newVal) { // 观察message变化 console.log('消息变化了:', newVal); } } })
-
状态管理:Redux中的store通知所有订阅的组件
-
DOM事件:浏览器内置的事件系统
javascriptbutton.addEventListener('click', handler); // 主题(button)直接通知观察者(handler)
性能与复杂度权衡
发布订阅模式在大型系统中优势明显:
- 支持更复杂的事件处理(过滤、转换、优先级)
- 完全解耦使系统更易扩展
- 但引入中间层带来轻微性能开销
观察者模式在简单场景更高效:
- 直接通知减少中间环节
- 实现简单明了
- 但当观察者数量巨大时,直接遍历列表可能成为性能瓶颈
常见误区澄清
误区1:"浏览器事件是发布订阅"
❌ 实际上,浏览器的addEventListener
是观察者模式的实现:
- DOM元素(主题)直接维护监听器列表
- 事件触发时直接调用所有监听器
误区2:"Vue的EventBus是观察者模式"
❌ 实际上,Vue的EventBus是发布订阅的典型应用:
javascript
// 创建事件中心
const EventBus = new Vue();
// 组件A(发布者)
EventBus.$emit('data-updated', payload);
// 组件B(订阅者)
EventBus.$on('data-updated', handleData);
这里没有直接依赖,通过Vue实例作为事件中心通信。
总结:根据场景选择最佳方案
理解两种模式的核心区别后,我们可以得出以下实践建议:
- 当你需要完全解耦 的组件通信 → 选择发布订阅
- 当你处理明确的主从关系 → 选择观察者模式
- 在性能敏感的简单场景 → 观察者模式更高效
- 需要复杂事件处理时 → 发布订阅更灵活
发布订阅和观察者模式就像工具箱中的不同工具,没有绝对的优劣,只有适合的场景。真正优秀的开发者不仅会使用这些模式,更能理解其背后的设计哲学,根据实际需求灵活变通。
下次当你在代码中实现事件通信时,不妨先问自己:我的组件之间是像公众号和订阅者(发布订阅),还是像老师和学生(观察者)?这个简单的思考将帮助你选择最合适的设计模式。
思考题:在React的Context API中,Provider和Consumer之间的关系更接近哪种模式?为什么?欢迎在评论区分享你的见解!