发布订阅和观察者模式,差一层但差很多
观察者模式跟发布订阅,经常被混着说。不是一回事。中间差了一个调度中心,这一层的有无,直接决定系统能不能往大了做。
到底差在哪
观察者模式,直连。Subject 认识 Observer,Observer 也知道自己挂在谁身上。没中间商。
发布订阅呢,中间塞了个事件总线。发布者不知道谁在听,订阅者也不知道谁在说。大家只跟总线打交道。
观察者是面对面喊话,发布订阅是往公告栏贴通知。
项目小的时候感受不明显。一旦模块多起来,这个区别是质变的。
观察者模式:最朴素的通知机制
手写一个,20 行搞定:
class Subject {
private observers: Function[] = []
subscribe(fn: Function) {
this.observers.push(fn)
}
notify(data: any) {
// Subject 直接持有 Observer 引用,耦合在这里
this.observers.forEach(fn => fn(data))
}
}
const subject = new Subject()
subject.subscribe((val) => console.log('A 收到:', val))
subject.subscribe((val) => console.log('B 收到:', val))
subject.notify('hello') // A 和 B 都会被调用
够直接,够简单。问题也明显——Subject 和 Observer 强绑定。你想加个过滤?加个延迟?全得改 Subject 的代码。
像快递员直接送到手里。效率高,但快递员得记住所有人地址。地址一多,扛不住。
发布订阅:中间加一层
Node.js 的 EventEmitter 就是典型的发布订阅实现:
class EventBus {
private events: Map<string, Function[]> = new Map()
on(event: string, fn: Function) {
// 订阅者只认"事件名",不认发布者
const fns = this.events.get(event) || []
fns.push(fn)
this.events.set(event, fns)
}
emit(event: string, ...args: any[]) {
// 发布者也不知道谁在听
const fns = this.events.get(event) || []
fns.forEach(fn => fn(...args))
}
off(event: string, fn: Function) {
const fns = this.events.get(event) || []
this.events.set(event, fns.filter(f => f !== fn))
}
}
const bus = new EventBus()
// 模块 A 只管发
bus.emit('user:login', { id: 1, name: 'test' })
// 模块 B 只管收,不知道谁发的
bus.on('user:login', (user) => {
console.log('更新头像:', user.name)
})
// 模块 C 也收,跟 B 互不相识
bus.on('user:login', (user) => {
console.log('拉取消息列表:', user.id)
})
发布者和订阅者彻底不认识了。模块 A 不知道 B 存在,B 也不知道 C 存在。大家都跟中心打交道,互相透明。
快递驿站模式。寄件人扔驿站,取件人去驿站拿。寄件人永远不用管谁取了件。
代码层面的真实差异
// ===== 观察者模式 =====
subject.subscribe(observer) // observer 被 subject 直接引用
subject.notify(data) // subject 遍历自己的 observer 列表
// ===== 发布订阅 =====
bus.on('event', handler) // handler 注册在 bus 上,不在发布者身上
bus.emit('event', data) // 发布者调 bus,bus 去通知
// 观察者 → subject.observers 里存着所有 observer(强引用)
// 发布订阅 → bus.events 按事件名分组存 handler(间接引用)
Vue 为啥弃了 EventBus
Vue 2 那会儿,new Vue() 当 EventBus 用的写法到处都是。好使是好使。项目一大就炸。
调试地狱。 一个 bus.emit('update') 丢出去,谁在听?全项目 grep 一下能找到十几个 bus.on('update'),散在各种文件里。数据流向完全没法追。
内存泄漏。 组件销毁了,忘了 off,handler 还在 bus 上挂着。这 bug 不会马上炸,慢慢吃内存,等你发现已经晚了。
命名撞车。 两个模块都用了 data:update?恭喜,喜提一个极难排查的 bug。
Vue 3 直接把实例上的 $on、$off、$emit 砍了。不是这东西技术上不行,是工程上管不住。
RxJS Subject:观察者模式的加强版
RxJS 的 Subject 骨子里还是观察者模式,但套了一整套流处理能力。
import { Subject } from 'rxjs'
import { filter, debounceTime, map } from 'rxjs/operators'
const click$ = new Subject<MouseEvent>()
// Subject 既是 Observable(能被订阅),也是 Observer(能接收值)
// 跟普通 EventEmitter 的本质区别就在这
click$.pipe(
debounceTime(300), // 防抖 300ms
filter(e => e.target instanceof HTMLButton), // 只要按钮点击
map(e => ({ x: e.clientX, y: e.clientY })) // 只取坐标
).subscribe(pos => {
console.log('处理点击:', pos)
})
document.addEventListener('click', e => click$.next(e))
看出来了吗?EventEmitter 只能"收到就执行"。Subject 可以在"收到"和"执行"之间塞任意多处理步骤。过滤、映射、合并、节流——全在管道里搞。
从"事件通知"到"事件流",是个质的变化。
Subject 的几个变体
别搞混了。
import { Subject, BehaviorSubject, ReplaySubject, AsyncSubject } from 'rxjs'
// Subject:最基础,订阅之后才能收到值
const s1 = new Subject<number>()
s1.next(1) // 没人听,丢了
s1.subscribe(v => {}) // 从现在起才能收到
// BehaviorSubject:有"当前值",新订阅者马上拿到最新的
const s2 = new BehaviorSubject<number>(0) // 必须给初始值
s2.next(1)
s2.next(2)
s2.subscribe(v => console.log(v)) // → 立刻输出 2
// ReplaySubject:回放最近 N 个值
const s3 = new ReplaySubject<number>(3) // 缓存最近 3 个
s3.next(1)
s3.next(2)
s3.next(3)
s3.next(4)
s3.subscribe(v => console.log(v)) // → 输出 2, 3, 4
// AsyncSubject:只在 complete() 时吐出最后一个值
// 适合只关心最终结果的场景,比如 HTTP 请求
const s4 = new AsyncSubject<number>()
s4.next(1)
s4.next(2)
s4.complete()
s4.subscribe(v => console.log(v)) // → 只输出 2
BehaviorSubject 在状态管理里用得最多。天然就是个可观察的状态容器。
分层来看:三层事件架构
真实项目里不是选一个模式就完了。得分层。
┌─────────────────────────────────────────┐
│ 组件层:观察者模式(直连,局部通信) │
│ → props/emit、ref 回调、Vue watch │
├─────────────────────────────────────────┤
│ 模块层:发布订阅(解耦,跨模块通信) │
│ → EventEmitter、mitt、tiny-emitter │
├─────────────────────────────────────────┤
│ 系统层:响应式流(可编排,全局状态流) │
│ → RxJS Subject、Redux Observable │
└─────────────────────────────────────────┘
组件层用观察者。 父子组件之间 props 往下传、emit 往上抛。直连高效,数据流清楚。父子关系本身就是强耦合的,不需要解耦。
模块层用发布订阅。 购物车和用户模块要通信?别互相 import。搞个 EventBus,各管各的。
系统层用响应式流。 WebSocket 消息进来要做鉴权、数据转换、错误重试、多播分发——这种复杂异步编排,只有流式处理扛得住。
import { webSocket } from 'rxjs/webSocket'
import { retry, share, filter, map } from 'rxjs/operators'
const ws$ = webSocket<any>('wss://api.example.com').pipe(
retry({ count: 3, delay: 1000 }), // 断线重连 3 次
share() // 多播,多个订阅者共享一个连接
)
// 订单模块只管订单消息
const order$ = ws$.pipe(
filter(msg => msg.type === 'order'),
map(msg => msg.payload)
)
// 通知模块只管通知消息
const notification$ = ws$.pipe(
filter(msg => msg.type === 'notification'),
map(msg => msg.payload)
)
order$.subscribe(handleOrder)
notification$.subscribe(showNotification)
选哪个
| 维度 | 观察者模式 | 发布订阅 | RxJS Subject |
|---|---|---|---|
| 耦合度 | 高(直连) | 低(经过中心) | 中(管道连接) |
| 调试难度 | 低 | 高(事件追踪难) | 中(有 operator 栈) |
| 学习成本 | 几乎没有 | 低 | 高(operator 上百个) |
| 异步编排 | 不行 | 不行 | 强 |
| 内存管理 | 手动 | 手动且容易漏 | unsubscribe 机制完善 |
| 适合规模 | 小 | 中 | 中到大 |
没有银弹。
小项目上 RxJS——杀鸡用牛刀。大项目只用 EventEmitter——给自己挖坑。
判断标准其实很简单:你的事件需不需要"处理"? 只是"A 发生了,通知 B",EventEmitter 够了。"A 发生了,等 300ms 看有没有 B,合并过滤一下再通知 C"——那得上 RxJS。
内存泄漏:公共敌人
不管用哪个模式,忘了取消订阅就是泄漏。这个没得商量。
// ❌ 经典泄漏
useEffect(() => {
bus.on('update', handleUpdate)
// 组件卸载了,handler 还挂着
// bus 不会被 GC,handler 引用的闭包变量也不会被 GC
}, [])
// ✅ 必须清理
useEffect(() => {
bus.on('update', handleUpdate)
return () => bus.off('update', handleUpdate)
}, [])
// ✅ RxJS 更干净
useEffect(() => {
const sub = source$.subscribe(handleUpdate)
return () => sub.unsubscribe()
}, [])
RxJS 这点做得好一些。Subscription 可以 add 子订阅,一次 unsubscribe 全清掉。EventEmitter 的 off 需要传同一个函数引用,匿名函数根本没法 off。坑不少。
什么时候别用事件驱动
事件驱动不是万能药。
需要同步拿返回值。 emit 是单向的,const result = emit('calculate') 这种写不了。非要搞可以塞回调,但那跟直接调函数有啥区别。
强顺序依赖。 事件天生松散,监听器执行顺序不该被依赖。如果你发现自己在纠结"handler A 必须在 B 之前跑"——别用事件了,老实写调用链。
高频数据同步。 每秒几千次?事件系统的开销——创建事件对象、遍历监听器——会变成瓶颈。这种场景直接上 SharedArrayBuffer。
解耦是个光谱
直接调用 ←→ 观察者 ←→ 发布订阅 ←→ 消息队列
耦合度递减 ────────────────────────→
可追踪性递减 ──────────────────────→
灵活性递增 ────────────────────────→
往左走,代码好追踪但改起来牵一发动全身。往右走,模块独立但调试时想摔键盘。
5 人团队的中后台项目,mitt 糊个轻量 EventBus 就够了。50 人团队搞实时协作,不上 RxJS 级别的流处理基本是在跟自己过不去。
选型就看一件事:你团队的复杂度天花板在哪。别提前优化,也别等扛不住了才重构——那时候成本翻十倍。