一、观察者模式的基本原理与数组实现思路
原理概述
在软件设计中,观察者模式通过让一个对象(被观察者)维护一个对其状态感兴趣的对象集合(观察者)来实现解耦。发布-订阅机制将事件源与处理逻辑分离,便于扩展和测试。
核心思想是:被观察者在状态变化时,遍历订阅者集合并执行回调,从而触发观察者的响应。对于前端开发者而言,这提供了一种在多个组件之间进行松耦合通信的高效方案。

用数组实现的要点
使用一个简单的数组来存放所有的订阅者回调函数,添加、删除、遍历都围绕这个数组进行。通过数组实现,可以实现快速原型并在浏览器端高效运行。
实现要点包括:订阅时将回调函数推入数组、取消订阅时从数组中移除、触发时对数组做迭代并调用每个回调,确保不会因为一个回调的异常而中断其他订阅者。
// 简单的观察者模式实现,使用数组保存回调
class Subject {constructor() {this.subscribers = [];}subscribe(cb) {if (typeof cb === 'function' && this.subscribers.indexOf(cb) === -1) {this.subscribers.push(cb);}}unsubscribe(cb) {this.subscribers = this.subscribers.filter(fn => fn !== cb);}notify(data) {// 通过 try-catch 保护每个回调,避免单个错误影响其他订阅者this.subscribers.forEach(cb => {try {cb(data);} catch (e) {// 可以在开发阶段输出错误信息console.error('Observer error:', e);}});}
}// 使用示例
const subject = new Subject();
function onChange(value) { console.log('接收到变化:', value); }
subject.subscribe(onChange);
subject.notify('状态更新A');
subject.unsubscribe(onChange);
subject.notify('状态更新B');
二、基于 JavaScript 数组的实现细节
数据结构设计
核心数据结构仍然是一个一维数组 subscribers,用于收集回调函数。为了支持更灵活的调用,可以把回调封装成对象,包含回调函数和上下文信息。
通过将订阅者实现为简单对象,可携带上下文,从而在回调中保持正确的 this 指向,提升实战中的易用性。
事件注册与触发机制
注册订阅时应做 去重检查,避免重复回调导致多次触发。触发阶段要保证在浏览器的事件循环中进行,避免阻塞主线程。
// 带去重的订阅实现
class Subject {constructor() {this.subscribers = [];}subscribe(cb) {if (typeof cb !== 'function') return;if (this.subscribers.indexOf(cb) === -1) {this.subscribers.push(cb);}}unsubscribe(cb) {this.subscribers = this.subscribers.filter(fn => fn !== cb);}notify(data) {for (const cb of this.subscribers) {cb(data);}}
}// 使用示例
const subject = new Subject();
const a = data => console.log('A:', data);
const b = data => console.log('B:', data);subject.subscribe(a);
subject.subscribe(b);
subject.notify('事件X');
subject.unsubscribe(a);
subject.notify('事件Y');
三、从原理到实战:完整代码实现
完整实现:带上下文的事件分发
在实际前端场景中,往往需要订阅者带有自己的上下文。下面给出一个可携带上下文的实现,兼容组合使用场景:
该实现通过将订阅者封装为对象,确保回调执行时的 this 指向正确,并提供一次性订阅与手动取消的能力,用于复杂组件通信。
class EventDispatcher {constructor() {this.subscriptions = [];}// cb: 回调函数, ctx: 回调上下文, options: { once: boolean }on(cb, ctx = null, options = {}) {if (typeof cb !== 'function') return;const sub = { cb, ctx, once: !!options.once };// 去重:同一个回调在同一上下文下只能订阅一次if (!this.subscriptions.find(s => s.cb === cb && s.ctx === ctx)) {this.subscriptions.push(sub);}}off(cb, ctx = null) {this.subscriptions = this.subscriptions.filter(s => !(s.cb === cb && s.ctx === ctx));}emit(data) {// 复制一份,防止在回调中修改订阅列表const subs = this.subscriptions.slice();for (const s of subs) {try {s.cb.call(s.ctx, data);} catch (e) {console.error('Dispatch error:', e);}if (s.once) {this.off(s.cb, s.ctx);}}}
}// 使用示例
const dispatcher = new EventDispatcher();
const logger = function(msg) { console.log('[log]', this.prefix, msg); };
const moduleA = { prefix: 'A' };dispatcher.on(logger, moduleA);
dispatcher.emit('数据更新1');
dispatcher.emit('数据更新2');
dispatcher.off(logger, moduleA);
dispatcher.emit('数据更新3');
使用示例:在前端场景中的演示
在组件化框架中,通过事件分发器实现跨组件通信,例如按钮组件触发事件,多个展示组件订阅并更新显示。
要点在于确保订阅生命周期与组件销毁同步,避免内存泄漏,以及在大量订阅者场景下的性能控制。
// 简易前端场景示例:按钮与两个展示组件通信
class Dispatcher extends EventDispatcher {}const dispatcher = new Dispatcher();function updatePrice(price) {console.log('[PricePanel] 更新价格:', price);
}
function updateStock(stock) {console.log('[StockPanel] 更新库存:', stock);
}// 订阅
dispatcher.on(updatePrice, null);
dispatcher.on(updateStock, null);// 触发
dispatcher.emit({ price: 199, stock: 42 });// 作为清理步骤,在组件销毁时取消订阅
dispatcher.off(updatePrice, null);
dispatcher.off(updateStock, null);
四、在前端项目中的应用场景
状态管理与组件通信
在大型前端项目中,观察者模式结合数组实现的事件分发可以作为轻量级的状态管理与跨组件通信方案。通过订阅者数组,组件能够在状态变更时接收通知,并进行局部或全局的渲染更新。
与框架自带的数据流方案相比,这种实现更为轻量,易于按需嵌入到现有组件库中,同时减少了对全局状态的依赖。
性能考虑与优化
使用数组实现时,订阅者数量的增长直接影响通知的成本。可以在实现中引入节流、去重和批量通知等技术,确保 UI 更新保持流畅。
另外,在高并发场景下,对回调中的异常进行保护,避免单个错误导致所有订阅者被跳过,从而维持健壮性。对于浏览器环境,合理使用异步通知可以降低主线程压力。


