发布订阅模式是 JavaScript 中最核心的设计模式之一。理解并手写一个完整的发布订阅系统,是每个前端开发者进阶的必经之路。
前言:为什么需要发布订阅模式?
我们先想象这样一个场景:当多个模块都需要响应同一个状态变化,这时候怎么处理呢?
传统硬编码依赖
class ModuleA {
update(data) {
ModuleB.render(data);
ModuleC.refresh(data);
ModuleD.notify(data);
// ...
}
}
这种情况下,每增加一个依赖模块,都要修改一次这里的代码。而且,如果有 100 个依赖模块,我们就要写 100 行调用语句,这显然是不合理的!
那么,正确的处理方式应该是什么呢?那就是发布订阅模式,解耦发布者和订阅者:
- 发布者只负责发布事件,不关心谁在监听
- 订阅者各自注册自己关心的事件
发布订阅模式
class EventBus {
// 在这里实现事件的发布和订阅
}
const eventBus = new EventBus();
// 订阅者各自注册自己关心的事件
eventBus.on('dataUpdate', (data) => ModuleB.render(data));
eventBus.on('dataUpdate', (data) => ModuleC.refresh(data));
eventBus.on('dataUpdate', (data) => ModuleD.notify(data));
// 发布者只负责发布事件,不关心谁在监听
eventBus.emit('dataUpdate', newData);
理解发布订阅模式
核心概念
- 发布者:只负责发布事件,不关心谁订阅
- 订阅者:只关心自己需要的事件
- 事件通道:存储事件和回调函数的对应关系
工作流
- 订阅者通过 on 方法注册事件监听器
- 发布者通过 emit 方法触发事件
- 事件通道找到所有对应的回调函数并执行
- 订阅者可以通过 off 方法取消订阅
优点
- 解耦:发布者和订阅者互不知道对方的存在
- 扩展性:新增订阅者无需修改发布者代码
- 异步:支持事件的延迟触发和异步处理
- 灵活:支持一次性监听、条件监听等高级特性
基础事件总线实现
简单实现
class SimpleEventBus {
constructor() {
this.events = {}; // 存储事件名到回调函数数组的映射
}
/**
* 订阅事件
* @param {string} eventName - 事件名
* @param {Function} callback - 回调函数
*/
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
console.log(`事件 [${eventName}] 订阅成功,当前订阅者: ${this.events[eventName].length}`);
}
/**
* 发布事件
* @param {string} eventName - 事件名
* @param {...any} args - 传递给回调的参数
*/
emit(eventName, ...args) {
const callbacks = this.events[eventName];
if (!callbacks || callbacks.length === 0) {
console.log(`事件 [${eventName}] 没有订阅者`);
return false;
}
console.log(`事件 [${eventName}] 发布,通知 ${callbacks.length} 个订阅者`);
callbacks.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`事件 [${eventName}] 回调执行错误:`, error);
}
});
return true;
}
/**
* 获取事件的订阅者数量
*/
listenerCount(eventName) {
return this.events[eventName]?.length || 0;
}
/**
* 获取所有事件名称
*/
eventNames() {
return Object.keys(this.events);
}
}
支持 once 一次性监听
class OnceEventBus extends SimpleEventBus {
/**
* 一次性订阅,执行一次后自动移除
*/
once(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 包装原回调,在调用后自动移除
const onceWrapper = (...args) => {
this.off(eventName, onceWrapper);
callback(...args);
};
// 保存原始回调的引用,以便取消订阅时能找到
onceWrapper.originalCallback = callback;
this.events[eventName].push(onceWrapper);
console.log(`事件 [${eventName}] 一次性订阅成功`);
}
/**
* 取消订阅
*/
off(eventName, callback) {
const callbacks = this.events[eventName];
if (!callbacks) return false;
// 如果没有指定回调,移除该事件的所有订阅
if (!callback) {
delete this.events[eventName];
console.log(`事件 [${eventName}] 所有订阅已移除`);
return true;
}
// 移除指定的回调
const index = callbacks.findIndex(cb =>
cb === callback || cb.originalCallback === callback
);
if (index !== -1) {
callbacks.splice(index, 1);
console.log(`事件 [${eventName}] 指定订阅已移除`);
// 如果该事件没有订阅者了,清理空数组
if (callbacks.length === 0) {
delete this.events[eventName];
}
return true;
}
return false;
}
/**
* 移除所有事件的所有订阅
*/
removeAllListeners() {
this.events = {};
console.log('所有事件订阅已清空');
}
}
Vue3 风格的事件总线
上述版本的代码其实已经能覆盖80%的业务场景了,但是在 Vue3 中,它缺不再推荐使用$on / $emit,而是改用 mitt ,这是为什么呢?
为什么Vue3不再推荐 emit,而是用mitt?
| 维度 | 传统EventBus | Vue3响应式 |
|---|---|---|
| 触发方式 | 显式emit | 隐式set |
| 数据结构 | 事件名+回调 | 状态+副作用 |
| 取消订阅 | 手动off | 自动收集依赖 |
从上表的对比中,我们可以看出传统 EventBus 和 Vue3 的事件总线有个显著的区别:Vue3 的事件总线是基于 状态+副作用 ,可以将其称为“有状态的事件”。
基本框架
class ReactiveEventBus {
constructor() {
this.events = new Map();
this.history = new Map();
this.lastEvent = new Map();
}
/**
* 创建响应式事件
*/
createReactiveEvent(eventName) {}
/**
* 创建带状态的响应式事件
*/
createStatefulEvent(eventName, initialState = null) {}
/**
* 创建计算事件
*/
computed(deps, compute) {}
/**
* 创建事件管道
*/
pipe(...transforms) {}
}
创建响应式事件createReactiveEvent
createReactiveEvent(eventName) {
let listeners = this.events.get(eventName);
if (!listeners) {
listeners = new Set();
this.events.set(eventName, listeners);
// 创建历史记录
this.history.set(eventName, []);
// 创建最近事件记录
this.lastEvent.set(eventName, null);
}
return {
// 监听事件
on: (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
},
// 一次性监听
once: (callback) => {
const wrapper = (...args) => {
callback(...args);
listeners.delete(wrapper);
};
listeners.add(wrapper);
return () => listeners.delete(wrapper);
},
// 触发事件
emit: (...args) => {
const timestamp = Date.now();
// 记录到历史
const history = this.history.get(eventName);
history.push({ timestamp, args });
// 限制历史长度
if (history.length > 50) {
history.shift();
}
// 更新最近事件
this.lastEvent.set(eventName, { timestamp, args });
// 触发监听器
listeners.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`事件 [${eventName}] 错误:`, error);
}
});
},
// 获取历史
history: () => this.history.get(eventName),
// 获取最近事件
last: () => this.lastEvent.get(eventName),
// 清除历史
clearHistory: () => this.history.set(eventName, []),
// 移除所有监听器
off: () => {
listeners.clear();
this.events.delete(eventName);
}
};
}
创建带状态的响应式事件createStatefulEvent
createStatefulEvent(eventName, initialState = null) {
const event = this.createReactiveEvent(eventName);
let state = initialState;
// 增强事件对象
return {
...event,
// 获取状态
get state() {
return state;
},
// 更新状态并触发事件
setState(newState) {
const oldState = state;
state = typeof newState === 'function'
? newState(state)
: newState;
event.emit(state, oldState);
return state;
},
// 订阅状态变化
subscribe(callback) {
return event.on(callback);
},
// 获取当前状态
snapshot: () => state
};
}
创建计算事件computed
computed(deps, compute) {
const resultEvent = this.createReactiveEvent(Symbol('computed'));
let lastResult = null;
const update = () => {
const newResult = compute();
if (newResult !== lastResult) {
lastResult = newResult;
resultEvent.emit(newResult);
}
};
// 订阅依赖
deps.forEach(dep => {
if (typeof dep.on === 'function') {
dep.on(update);
}
});
// 立即计算一次
update();
return {
value: () => lastResult,
on: resultEvent.on,
off: resultEvent.off
};
}
创建事件管道pipe
pipe(...transforms) {
return (eventName) => {
const source = this.createReactiveEvent(eventName);
const target = this.createReactiveEvent(`${eventName}:pipe`);
source.on((...args) => {
let result = args;
for (const transform of transforms) {
result = transform(result);
}
target.emit(result);
});
return {
source,
target
};
};
}
完整实现
class ReactiveEventBus {
constructor() {
this.events = new Map();
this.history = new Map();
this.lastEvent = new Map();
}
/**
* 创建响应式事件
*/
createReactiveEvent(eventName) {
let listeners = this.events.get(eventName);
if (!listeners) {
listeners = new Set();
this.events.set(eventName, listeners);
// 创建历史记录
this.history.set(eventName, []);
// 创建最近事件记录
this.lastEvent.set(eventName, null);
}
return {
// 监听事件
on: (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
},
// 一次性监听
once: (callback) => {
const wrapper = (...args) => {
callback(...args);
listeners.delete(wrapper);
};
listeners.add(wrapper);
return () => listeners.delete(wrapper);
},
// 触发事件
emit: (...args) => {
const timestamp = Date.now();
// 记录到历史
const history = this.history.get(eventName);
history.push({ timestamp, args });
// 限制历史长度
if (history.length > 50) {
history.shift();
}
// 更新最近事件
this.lastEvent.set(eventName, { timestamp, args });
// 触发监听器
listeners.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`事件 [${eventName}] 错误:`, error);
}
});
},
// 获取历史
history: () => this.history.get(eventName),
// 获取最近事件
last: () => this.lastEvent.get(eventName),
// 清除历史
clearHistory: () => this.history.set(eventName, []),
// 移除所有监听器
off: () => {
listeners.clear();
this.events.delete(eventName);
}
};
}
/**
* 创建带状态的响应式事件
*/
createStatefulEvent(eventName, initialState = null) {
const event = this.createReactiveEvent(eventName);
let state = initialState;
// 增强事件对象
return {
...event,
// 获取状态
get state() {
return state;
},
// 更新状态并触发事件
setState(newState) {
const oldState = state;
state = typeof newState === 'function'
? newState(state)
: newState;
event.emit(state, oldState);
return state;
},
// 订阅状态变化
subscribe(callback) {
return event.on(callback);
},
// 获取当前状态
snapshot: () => state
};
}
/**
* 创建计算事件
*/
computed(deps, compute) {
const resultEvent = this.createReactiveEvent(Symbol('computed'));
let lastResult = null;
const update = () => {
const newResult = compute();
if (newResult !== lastResult) {
lastResult = newResult;
resultEvent.emit(newResult);
}
};
// 订阅依赖
deps.forEach(dep => {
if (typeof dep.on === 'function') {
dep.on(update);
}
});
// 立即计算一次
update();
return {
value: () => lastResult,
on: resultEvent.on,
off: resultEvent.off
};
}
/**
* 创建事件管道
*/
pipe(...transforms) {
return (eventName) => {
const source = this.createReactiveEvent(eventName);
const target = this.createReactiveEvent(`${eventName}:pipe`);
source.on((...args) => {
let result = args;
for (const transform of transforms) {
result = transform(result);
}
target.emit(result);
});
return {
source,
target
};
};
}
}
结语
发布订阅模式是 JavaScript 异步编程的基石,掌握它的实现原理和应用场景,能够帮助我们写出更优雅、更可维护的代码。无论是构建大型应用还是开发 npm 包,这个模式都会是我们的重要工具。对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!