【高频考点精讲】发布订阅模式实现:从基础EventEmitter到复杂事件系统,掌握观察者模式

发布订阅模式:从EventEmitter到复杂事件系统,前端工程师必会的观察者模式

🧑‍🏫 作者:全栈老李

📅 更新时间:2025 年 5 月

🧑‍💻 适合人群:前端初学者、进阶开发者

🚀 版权:本文由全栈老李原创,转载请注明出处。

今天咱们聊聊前端开发中那个无处不在却又容易被忽视的设计模式——发布订阅模式。我是全栈老李,一个喜欢把复杂技术讲简单的技术博主。

发布订阅模式是什么?

想象一下你关注了一个公众号(比如”全栈老李”),每次我发新文章,你的微信就会收到通知。这就是发布订阅模式的现实例子——你不必主动来问我”有新文章了吗”,而是订阅后自动接收更新。

在前端开发中,发布订阅模式(也叫观察者模式)允许对象订阅其他对象的事件,并在事件发生时自动得到通知。这种松耦合的设计让代码更灵活、更易维护。

手写一个基础EventEmitter

让我们从零实现一个最简单的EventEmitter,理解其核心原理:

// 全栈老李的EventEmitter基础实现
class EventEmitter {
            
  constructor() {
            
    this.events = {
            }; // 存储事件和对应的回调函数
  }

  // 订阅事件
  on(eventName, callback) {
            
    if (!this.events[eventName]) {
            
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
    return this; // 链式调用
  }

  // 发布事件
  emit(eventName, ...args) {
            
    const callbacks = this.events[eventName];
    if (callbacks) {
            
      callbacks.forEach(cb => cb.apply(this, args));
    }
    return this; // 链式调用
  }

  // 取消订阅
  off(eventName, callback) {
            
    const callbacks = this.events[eventName];
    if (callbacks) {
            
      this.events[eventName] = callbacks.filter(cb => cb !== callback);
    }
    return this;
  }

  // 一次性订阅
  once(eventName, callback) {
            
    const wrapper = (...args) => {
            
      callback.apply(this, args);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
    return this;
  }
}

这个实现虽然简单,但包含了发布订阅模式的核心功能。全栈老李提醒你注意几个关键点:

events对象用事件名作为key,存储回调函数数组
on方法添加订阅者
emit方法触发事件并执行所有回调
off方法取消订阅
once实现一次性订阅

真实场景中的使用示例

让我们看一个电商网站的实际案例:

// 购物车事件系统 - 全栈老李实战示例
class ShoppingCart {
            
  constructor() {
            
    this.emitter = new EventEmitter();
    this.items = [];
  }

  addItem(item) {
            
    this.items.push(item);
    this.emitter.emit('itemAdded', item);
    this.emitter.emit('cartUpdated', this.items);
  }

  removeItem(itemId) {
            
    this.items = this.items.filter(item => item.id !== itemId);
    this.emitter.emit('itemRemoved', itemId);
    this.emitter.emit('cartUpdated', this.items);
  }
}

// 使用示例
const cart = new ShoppingCart();

// 订阅商品添加事件
cart.emitter.on('itemAdded', item => {
            
  console.log(`商品添加通知:${
              item.name} 已加入购物车`);
  // 这里可以更新UI通知、发送埋点等
});

// 订阅购物车更新事件
cart.emitter.on('cartUpdated', items => {
            
  console.log('购物车更新了,当前商品数量:', items.length);
  // 更新购物车图标数字、计算总价等
});

// 添加商品
cart.addItem({
             id: 1, name: 'JavaScript高级程序设计', price: 99 });

在这个例子中,购物车状态的改变不需要直接调用UI更新方法,而是通过事件通知所有关心这些变化的组件。全栈老李认为这种解耦设计让代码更容易维护和扩展。

进阶:更复杂的事件系统

实际项目中,我们可能需要更强大的事件系统。让我们增强基础实现:

// 全栈老李的增强版EventEmitter
class AdvancedEventEmitter extends EventEmitter {
            
  constructor() {
            
    super();
    this.maxListeners = 10; // 默认最大监听器数量
  }

  // 设置最大监听器数量
  setMaxListeners(n) {
            
    this.maxListeners = n;
    return this;
  }

  // 获取事件监听器数量
  listenerCount(eventName) {
            
    const callbacks = this.events[eventName];
    return callbacks ? callbacks.length : 0;
  }

  // 移除所有监听器
  removeAllListeners(eventName) {
            
    if (eventName) {
            
      delete this.events[eventName];
    } else {
            
      this.events = {
            };
    }
    return this;
  }

  // 异步触发事件
  async emitAsync(eventName, ...args) {
            
    const callbacks = this.events[eventName];
    if (callbacks) {
            
      await Promise.all(callbacks.map(cb => cb.apply(this, args)));
    }
    return this;
  }
}

增强版增加了几个实用功能:

监听器数量限制
异步事件触发
更灵活的监听器管理

发布订阅在前端生态中的应用

发布订阅模式在前端无处不在,全栈老李给你举几个典型例子:

DOM事件系统addEventListener就是最基础的发布订阅实现
Vue自定义事件$on, $emit方法
Redux:store.subscribe监听状态变化
WebSocket:消息的订阅与发布
Node.js EventEmitter:几乎所有核心模块都继承自它

性能优化与注意事项

虽然发布订阅很强大,但全栈老李提醒你要注意几个问题:

内存泄漏:忘记取消订阅会导致回调函数无法被垃圾回收
调试困难:事件流不直观时,问题难以追踪
过度使用:简单场景直接用回调可能更直接

优化建议:

// 好习惯:在组件销毁时取消订阅
class MyComponent {
            
  constructor() {
            
    this.handleResize = () => {
            /*...*/};
    window.addEventListener('resize', this.handleResize);
  }
  
  destroy() {
            
    window.removeEventListener('resize', this.handleResize);
  }
}

课后作业:手写支持异步的事件总线

来挑战一个面试常见题:实现一个支持异步事件处理的事件总线系统,要求:

支持同步和异步事件发布
支持事件优先级
支持中间件机制
支持事件拦截

// 全栈老李的作业模板
class AsyncEventBus {
            
  // 你的实现
}

// 测试用例
const bus = new AsyncEventBus();

bus.on('login', async (user) => {
            
  await new Promise(resolve => setTimeout(resolve, 100));
  console.log(`1. 记录登录日志: ${
              user.name}`);
}, {
             priority: 1 });

bus.on('login', (user) => {
            
  console.log(`2. 更新用户状态: ${
              user.name}`);
}, {
             priority: 2 });

(async () => {
            
  await bus.emit('login', {
             name: '全栈老李', id: 123 });
  console.log('3. 登录流程结束');
})();

/* 
期望输出顺序:
2. 更新用户状态: 全栈老李
1. 记录登录日志: 全栈老李
3. 登录流程结束
*/

把你的实现发在评论区,全栈老李会随机抽几位同学的代码进行点评!看看谁能写出最优雅的实现~

发布订阅模式是前端工程师必须掌握的核心设计模式,理解它不仅能写出更好的代码,还能更深入地理解前端生态中的各种库和框架。我是全栈老李,我们下期再见!

🔥 必看面试题

【3万字纯干货】前端学习路线全攻略!从小白到全栈工程师(2025版)

【初级】前端开发工程师面试100题(一)

【初级】前端开发工程师面试100题(二)

【初级】前端开发工程师的面试100题(速记版)

我是全栈老李,一个资深Coder!

写码不易,如果你觉得本文有收获,点赞 + 收藏走一波!感谢鼓励🌹🌹🌹

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容