【前端面试小册】JS-第24节:发布订阅模式进阶,实战运用
一、概述
1.1 定义
发布/订阅模式:基于一个主题/事件通道,希望接收通知的对象(订阅者)通过自定义事件订阅主题,被激活事件的对象(发布者)通过发布主题事件的方式被通知。
1.2 作用
- 广泛应用于异步编程中(替代了传递回调函数)
- 对象之间松散耦合的编写代码
高级前端必须掌握的知识点之一
1.3 核心思想
graph TD
A[发布者 Publisher] --> B[事件中心 Event Center]
B --> C[订阅者1 Subscriber1]
B --> D[订阅者2 Subscriber2]
B --> E[订阅者3 Subscriber3]
关键点:
- 发布者和订阅者不直接通信
- 通过事件中心进行通信
- 实现完全解耦
二、基础实现
2.1 基本结构
// 发布订阅模式
class EventEmitter {
constructor() {
// 事件对象,存放订阅的名字和事件
this.events = {};
}
// 订阅事件的方法
on(eventName, callback) {
if (!this.events[eventName]) {
// 注意是数组,一个名字可以订阅多个事件函数
this.events[eventName] = [callback];
} else {
// 存在则 push 到指定数组的尾部保存
this.events[eventName].push(callback);
}
}
// 触发事件的方法
emit(eventName, ...args) {
// 遍历执行所有订阅的事件
if (this.events[eventName]) {
this.events[eventName].forEach(cb => cb(...args));
}
}
// 移除订阅事件
removeListener(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
// 只执行一次订阅的事件,然后移除
once(eventName, callback) {
// 绑定的是 fn,执行的时候会触发 fn 函数
let fn = () => {
callback(); // fn 函数中调用原有的 callback
this.removeListener(eventName, fn); // 删除 fn,再次执行的时候之后执行一次
};
this.on(eventName, fn);
}
}
2.2 测试用例
const event = new EventEmitter();
const callback = () => {
console.log('愚公上岸说');
};
event.on('test', callback);
event.emit('test'); // 输出:愚公上岸说
三、完整实现(增强版)
3.1 完整功能实现
class EventEmitter {
constructor() {
// 事件对象,存放订阅的名字和事件
this.events = {};
}
// 订阅事件
on(eventName, callback) {
// 参数验证
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 避免重复订阅
if (!this.events[eventName].includes(callback)) {
this.events[eventName].push(callback);
}
// 返回 this,支持链式调用
return this;
}
// 触发事件
emit(eventName, ...args) {
if (this.events[eventName]) {
// 创建副本,避免在执行过程中修改原数组
const callbacks = [...this.events[eventName]];
callbacks.forEach(cb => {
try {
cb(...args);
} catch (error) {
console.error(`Error in event handler for ${eventName}:`, error);
}
});
}
return this;
}
// 移除订阅事件
removeListener(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
// 如果没有订阅者了,删除该事件
if (this.events[eventName].length === 0) {
delete this.events[eventName];
}
}
return this;
}
// 移除所有订阅
removeAllListeners(eventName) {
if (eventName) {
delete this.events[eventName];
} else {
this.events = {};
}
return this;
}
// 只执行一次订阅的事件,然后移除
once(eventName, callback) {
const fn = (...args) => {
callback(...args);
this.removeListener(eventName, fn);
};
this.on(eventName, fn);
return this;
}
// 获取指定事件的订阅者数量
listenerCount(eventName) {
return this.events[eventName] ? this.events[eventName].length : 0;
}
// 获取所有事件名
eventNames() {
return Object.keys(this.events);
}
}
3.2 使用示例
const event = new EventEmitter();
// 订阅事件
event.on('test', (data) => {
console.log('订阅者1:', data);
});
event.on('test', (data) => {
console.log('订阅者2:', data);
});
// 触发事件
event.emit('test', '愚公上岸说');
// 输出:
// 订阅者1: 愚公上岸说
// 订阅者2: 愚公上岸说
// 只执行一次
event.once('once', () => {
console.log('只执行一次');
});
event.emit('once'); // 输出:只执行一次
event.emit('once'); // 无输出
// 移除订阅
const callback = () => console.log('会被移除');
event.on('remove', callback);
event.removeListener('remove', callback);
event.emit('remove'); // 无输出
四、发布订阅 vs 观察者模式
4.1 区别
说到发布订阅肯定少不了提一下观察者模式,因为当你写完发布订阅后,面试官往往会问你,二者区别。其实二者在核心思想、运作机制上没有本质的差别。
核心区别:
| 特性 | 发布订阅模式 | 观察者模式 |
|---|---|---|
| 中间人 | 存在第三方(事件中心) | 不存在第三方 |
| 通信方式 | 发布者无法直接感知订阅者 | 发布者能直接感知订阅者 |
| 耦合度 | 完全解耦 | 部分耦合 |
4.2 类比理解
发布订阅模式:有三方,不直接通知
- 发布者将文件传到第三方平台,平台通知给订阅者
- 就像:你发布视频到 B 站,B 站通知所有订阅你的用户
观察者模式:无三方,直接通知
- 发布者拉一个微信群,直接将文件发给每一个人
- 就像:你在微信群里直接 @所有人
4.3 代码对比
观察者模式
// 观察者模式:发布者直接管理订阅者
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log('收到通知:', data);
}
}
// 使用
const subject = new Subject();
const observer = new Observer();
subject.addObserver(observer);
subject.notify('消息'); // 发布者直接通知观察者
发布订阅模式
// 发布订阅模式:通过事件中心通信
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(cb => cb(data));
}
}
}
// 使用
const event = new EventEmitter();
event.on('message', (data) => {
console.log('收到通知:', data);
});
event.emit('message', '消息'); // 通过事件中心通知
五、实际应用场景
5.1 Vue 的事件总线
// Vue 2.x 中使用事件总线
// main.js
Vue.prototype.$bus = new Vue();
// 组件 A:发布事件
this.$bus.$emit('user-login', userInfo);
// 组件 B:订阅事件
this.$bus.$on('user-login', (userInfo) => {
console.log('用户登录:', userInfo);
});
5.2 Node.js 的 EventEmitter
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('事件触发');
});
myEmitter.emit('event');
5.3 模块间通信
// 模块 A
import eventBus from './eventBus';
eventBus.emit('data-updated', newData);
// 模块 B
import eventBus from './eventBus';
eventBus.on('data-updated', (data) => {
updateUI(data);
});
5.4 解耦业务逻辑
// 用户登录后,需要更新多个模块
class UserService {
login(user) {
// 登录逻辑
eventBus.emit('user-login', user);
}
}
// 购物车模块
eventBus.on('user-login', (user) => {
loadCart(user.id);
});
// 消息模块
eventBus.on('user-login', (user) => {
loadMessages(user.id);
});
六、观察者模式解决了什么?
6.1 优点
减少模块间的耦合问题,有它在,即便是两个分离的、毫不相关的模块,也可以实现数据通信。
6.2 缺点
但是没有完全地解决耦合问题:被观察者必须去维护一套观察者的集合,这些观察者必须实现统一的方法供被观察者调用。
七、发布订阅解决了什么?
7.1 优点
发布者完全不用感知订阅者,事件的注册和触发都发生在独立于双方的第三方,实现了完全解耦。
7.2 应用场景
- 异步编程:替代回调函数
- 模块解耦:不同模块间通信
- 事件驱动:基于事件的架构设计
八、性能优化
8.1 内存泄漏防护
class EventEmitter {
constructor() {
this.events = {};
this.maxListeners = 10; // 默认最大监听器数量
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 检查监听器数量
if (this.events[eventName].length >= this.maxListeners) {
console.warn(`MaxListenersExceededWarning: ${eventName} has ${this.events[eventName].length} listeners`);
}
this.events[eventName].push(callback);
return this;
}
// 设置最大监听器数量
setMaxListeners(n) {
this.maxListeners = n;
return this;
}
}
8.2 批量操作
class EventEmitter {
// 批量订阅
onMany(events, callback) {
events.forEach(event => {
this.on(event, callback);
});
return this;
}
// 批量移除
removeMany(events, callback) {
events.forEach(event => {
this.removeListener(event, callback);
});
return this;
}
}
九、面试要点总结
核心知识点
- 发布订阅模式:通过事件中心实现发布者和订阅者的解耦
- 核心方法:on(订阅)、emit(发布)、removeListener(移除)、once(只执行一次)
- 与观察者模式的区别:是否有第三方事件中心
- 应用场景:异步编程、模块解耦、事件驱动
常见面试题
Q1: 发布订阅模式和观察者模式的区别?
答:
- 发布订阅模式:存在第三方事件中心,发布者和订阅者完全解耦
- 观察者模式:不存在第三方,发布者直接管理订阅者,部分耦合
Q2: 如何实现发布订阅模式?
答:使用对象存储事件名和回调函数数组,提供 on 方法订阅、emit 方法发布、removeListener 方法移除订阅。
Q3: 发布订阅模式解决了什么问题?
答:解决了模块间耦合问题,实现了完全解耦,让不同模块可以通过事件中心进行通信。
实战建议
- ✅ 理解发布订阅的核心思想
- ✅ 掌握基本实现方法
- ✅ 注意内存泄漏问题(及时移除订阅)
- ✅ 理解与观察者模式的区别