讲解
关注了博主,他一发新视频你就收到推送——你不用每隔五分钟去他主页刷一次。观察者模式就是这个「关注-推送」机制:定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。被关注的叫主题(Subject),关注的叫观察者(Observer)。
它解决的耦合问题非常具体:没有观察者时,订单系统支付成功后要依次调用发短信、加积分、通知仓库、更新推荐——每加一个下游动作,支付模块就要改代码、多认识一个模块。有了事件通知,支付模块只喊一嗓子「我成功了」,谁关心谁自己订阅,支付模块对下游一无所知。
聊聊它和「发布订阅」的细微差别,这是面试高频题。经典观察者模式里,主题直接持有观察者列表并逐个调用(二者互相认识,虽然是抽象层面的认识);发布订阅(pub/sub)在两者之间加了一个事件中心/消息代理,发布者和订阅者彼此彻底不见面,连抽象层面都不认识。Node.js 的 EventEmitter、前端自定义事件总线严格说是发布订阅;而「主题直接管理观察者」的经典形态在库内部(如 Vue 的依赖收集)更常见。结构差别不大,耦合程度的差别是真的。
观察者模式是前端世界的地基之一:DOM 事件、响应式框架的数据绑定、Redux 的 subscribe,全是它的变体。学它要顺带建立一个警觉:事件链路过长时(A 发事件触发 B 再发事件……),调试会变成「谁在什么时候触发了什么」的侦探游戏,所以复杂链路要配日志或事件追踪。
示例
手写一个带类型安全的事件总线:订阅、退订、按事件名分发,用断言验证通知语义:
import assert from 'node:assert/strict';
type Listener<T> = (payload: T) => void;
// 主题:维护「事件名 -> 观察者集合」,变化时逐一通知
class EventBus<Events> {
private readonly listeners = new Map<keyof Events, Set<Listener<never>>>();
// 订阅;返回退订函数
on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void {
let set = this.listeners.get(event);
if (set === undefined) {
set = new Set();
this.listeners.set(event, set);
}
set.add(listener as Listener<never>);
return () => {
set.delete(listener as Listener<never>);
};
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
const set = this.listeners.get(event);
if (set === undefined) return;
for (const listener of set) {
(listener as Listener<Events[K]>)(payload);
}
}
}
// 事件与载荷的契约:TypeScript 让 emit 和 on 的参数自动对得上
interface AppEvents {
'user:login': { name: string };
'order:paid': { amount: number };
}
const bus = new EventBus<AppEvents>();
const log: string[] = [];
const off = bus.on('user:login', (u) => log.push('欢迎 ' + u.name));
bus.on('user:login', (u) => log.push('发送新手礼包给 ' + u.name));
bus.on('order:paid', (o) => log.push('订单入账 ' + o.amount + ' 元'));
bus.emit('user:login', { name: '小林' });
bus.emit('order:paid', { amount: 99 });
off(); // 退订「欢迎」观察者
bus.emit('user:login', { name: '小陈' });
assert.deepEqual(log, ['欢迎 小林', '发送新手礼包给 小林', '订单入账 99 元', '发送新手礼包给 小陈']);
console.log(log.join('\n'));
断言序列说明三件事:一个事件可以有多个观察者(登录触发了两条);不同事件互不串扰(支付没有惊动登录观察者);退订后不再收到通知(小陈登录时没有「欢迎」)。泛型参数 Events 让事件名和载荷类型绑定——emit('user:login', { amount: 99 }) 这种张冠李戴会被编译器拦下。
常见坑
- 忘记退订导致内存泄漏:观察者被主题持有引用,组件销毁不退订就永远无法回收;on 返回退订函数就是为了配合生命周期。
- 观察者里抛异常打断后续通知:一个监听器炸了,排在它后面的观察者收不到通知;主题分发时应逐个 try-catch 或改用异步投递。
- 事件里再发事件造成循环:A 监听「保存成功」又 emit「刷新」,「刷新」的处理里又触发「保存成功」——无限循环;事件要有明确的方向性约定。
- 用字符串事件名却无注册表:事件名散落各处拼错一个字母,静默收不到通知;像示例一样用类型约束,或至少集中定义常量。
- 同步分发处理重活:观察者里做网络请求会拖住 emit 的调用方;重活应交给队列异步消化。
小结
观察者/发布订阅实现「一变多知」的解耦通知;经典形态主题直连观察者,pub/sub 隔一层事件中心;类型化的事件契约 + 成对的订阅/退订是工程标配。下一章:把「状态」本身变成对象的状态模式。