发布订阅的实现
七崽爱吃小饼干2026/01/30阅读 2专栏 JavaScript
发布订阅模式,这个模式的核心是解耦事件的发布者和订阅者,让它们之间不直接通信,而是通过一个事件中心来管理。
实现思路
发布订阅模式主要包含三个核心角色和对应的功能:
- 事件中心(EventEmitter):存储事件与对应回调函数的映射关系,提供订阅、取消订阅、发布的方法。
- 订阅者:通过
on方法订阅事件,并注册回调函数。 - 发布者:通过
emit方法发布事件,触发该事件下所有订阅的回调函数。
同时还需要考虑:
- 一个事件可以绑定多个回调函数
- 支持取消指定事件的指定回调
- 支持只执行一次的订阅(
once方法)
完整实现代码
javascript
class EventEmitter {
constructor() {
// 存储事件和回调的映射:{ eventName: [fn1, fn2, ...] }
this.events = Object.create(null);
}
/**
* 订阅事件
* @param {string} eventName - 事件名称
* @param {Function} callback - 回调函数
* @returns {EventEmitter} 返回实例,支持链式调用
*/
on(eventName, callback) {
// 校验参数合法性
if (typeof eventName !== 'string' || typeof callback !== 'function') {
throw new Error('事件名必须是字符串,回调必须是函数');
}
// 如果事件不存在,初始化一个空数组
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 避免重复订阅同一个回调
if (!this.events[eventName].includes(callback)) {
this.events[eventName].push(callback);
}
return this;
}
/**
* 发布事件(触发回调)
* @param {string} eventName - 事件名称
* @param {...any} args - 传递给回调的参数
* @returns {EventEmitter} 返回实例,支持链式调用
*/
emit(eventName, ...args) {
if (typeof eventName !== 'string') {
throw new Error('事件名必须是字符串');
}
// 如果事件没有订阅者,直接返回
if (!this.events[eventName]) return this;
// 遍历执行所有回调,并传递参数
this.events[eventName].forEach(callback => {
// 确保回调执行时的 this 指向 EventEmitter 实例(可选,根据需求调整)
callback.call(this, ...args);
});
return this;
}
/**
* 取消订阅
* @param {string} eventName - 事件名称
* @param {Function} [callback] - 可选,指定要取消的回调;不传则取消该事件所有回调
* @returns {EventEmitter} 返回实例,支持链式调用
*/
off(eventName, callback) {
if (typeof eventName !== 'string') {
throw new Error('事件名必须是字符串');
}
// 事件不存在,直接返回
if (!this.events[eventName]) return this;
if (typeof callback === 'function') {
// 只取消指定的回调
this.events[eventName] = this.events[eventName].filter(fn => fn !== callback);
} else {
// 取消该事件所有回调
this.events[eventName] = [];
}
return this;
}
/**
* 订阅一次事件(执行后自动取消)
* @param {string} eventName - 事件名称
* @param {Function} callback - 回调函数
* @returns {EventEmitter} 返回实例,支持链式调用
*/
once(eventName, callback) {
if (typeof eventName !== 'string' || typeof callback !== 'function') {
throw new Error('事件名必须是字符串,回调必须是函数');
}
// 包装一个一次性回调
const onceCallback = (...args) => {
callback.call(this, ...args); // 执行原回调
this.off(eventName, onceCallback); // 执行后取消订阅
};
this.on(eventName, onceCallback);
return this;
}
}
代码使用示例
javascript
// 1. 创建事件中心实例
const emitter = new EventEmitter();
// 2. 定义回调函数
function handleMsg(msg) {
console.log('收到消息:', msg);
}
// 3. 订阅事件
emitter.on('message', handleMsg);
// 4. 发布事件(触发回调)
emitter.emit('message', 'Hello 发布订阅!'); // 输出:收到消息:Hello 发布订阅!
// 5. 订阅一次性事件
emitter.once('onceEvent', (num) => {
console.log('一次性事件触发:', num);
});
emitter.emit('onceEvent', 123); // 输出:一次性事件触发:123
emitter.emit('onceEvent', 456); // 无输出(已自动取消订阅)
// 6. 取消指定回调的订阅
emitter.off('message', handleMsg);
emitter.emit('message', '取消后不会输出'); // 无输出
// 7. 取消事件所有回调
emitter.on('test', () => console.log('test1'));
emitter.on('test', () => console.log('test2'));
emitter.off('test'); // 取消test事件所有回调
emitter.emit('test'); // 无输出
核心代码解释
constructor:初始化events对象,用于存储事件和回调的映射,使用Object.create(null)避免继承原型链上的属性。on方法:校验参数后,将回调函数添加到对应事件的回调数组中,同时避免重复订阅。emit方法:触发指定事件的所有回调,并将参数传递给回调函数,保证回调执行时的this指向(可选)。off方法:支持取消指定回调或整个事件的所有回调,通过filter过滤掉要取消的回调。once方法:包装原回调,执行后自动调用off取消订阅,实现“只执行一次”的效果。
总结
- 发布订阅模式的核心是事件中心,通过
events对象维护事件与回调的映射,解耦发布者和订阅者。 - 核心方法包括:
on(订阅)、emit(发布)、off(取消订阅),扩展方法once(一次性订阅)是常用增强。 - 实现时需注意参数校验、避免重复订阅、回调参数传递、
this指向等细节,保证代码的健壮性。