Event事件解耦
1100字约4分钟
2024-08-20
事件解耦
其实主要的就是降低各个模块之间耦合度,减少改模块牵动一堆东西。ps:这里主要用发布订阅的模式进行解耦。
思路
- 可以将事件模块化,各个模块管理自己的事情
- 同一个事件,有可能会有多个逻辑触发
- 后期维护也只需要维护事件的新增等问题,不需要改动其他模块。
代码实现
- 建议分多个模块,一个模块处理多个事件后期维护有点麻烦。
- 不同模块事件中心,处理不同模块事件。
/**
* @class TypedEventEmitter
* @description 事件派发器基类。
*/
export class TypedEventEmitter<T_Events extends Record<string | symbol, any>> {
private _events: Map<keyof T_Events, ((...args: any[]) => void)[]>;
constructor() {
this._events = new Map();
}
/**
* 监听一个事件。
* @param eventName - 事件名称 (享受类型提示)
* @param listener - 监听器函数 (参数享受类型提示)
* @returns {this}
*/
public on<K extends keyof T_Events>(eventName: K, listener: (payload: T_Events[K]) => void): this {
if (!this._events.has(eventName)) {
this._events.set(eventName, []);
}
const listeners = this._events.get(eventName)!;
if (!listeners.includes(listener)) {
listeners.push(listener);
}
return this;
}
/**
* 添加一个只执行一次的监听器。
* @param eventName - 事件名称 (享受类型提示)
* @param listener - 监听器函数 (参数享受类型提示)
* @returns {this}
*/
public once<K extends keyof T_Events>(eventName: K, listener: (payload: T_Events[K]) => void): this {
const onceWrapper = (payload: T_Events[K]) => {
this.off(eventName, onceWrapper as any);
listener(payload);
};
// 附加一个属性,方便在off()方法中识别和移除
(onceWrapper as any).listener = listener;
this.on(eventName, onceWrapper);
return this;
}
/**
* 移除一个事件监听器。
* @param eventName - 事件名称
* @param listener - 监听器函数
* @returns {this}
*/
public off<K extends keyof T_Events>(eventName: K, listener: (payload: T_Events[K]) => void): this {
if (!this._events.has(eventName)) {
return this;
}
const listeners = this._events.get(eventName)!;
const originalListeners = listeners.filter((l) => l !== listener && (l as any).listener !== listener);
if (originalListeners.length < listeners.length) {
this._events.set(eventName, originalListeners as any);
}
return this;
}
/**
* 移除指定事件的所有监听器,或移除所有事件的所有监听器。
* @param eventName - (可选) 事件名称
* @returns {this}
*/
public removeAllListeners<K extends keyof T_Events>(eventName?: K): this {
if (eventName) {
this._events.delete(eventName);
} else {
this._events.clear();
}
return this;
}
/**
* 按顺序同步执行每个监听器。
* @param eventName - 事件名称 (享受类型提示)
* @param payload - 传递给监听器的数据 (享受类型提示)
* @returns {boolean} - 如果事件有监听器则返回true,否则返回false
*/
public emit<K extends keyof T_Events>(eventName: K, payload: T_Events[K]): boolean {
if (!this._events.has(eventName)) {
return false;
}
// 创建一个监听器数组的副本,以防在emit期间监听器被修改
const listeners = [...(this._events.get(eventName) || [])];
for (const listener of listeners) {
try {
listener(payload as any);
} catch (error) {
console.error(`Error in event listener for ${String(eventName)}:`, error);
// 如果定义了全局 error 事件,可以派发
if (this._events.has("error" as any)) {
this.emit("error" as any, error as any);
}
}
}
return true;
}
}api模块
import { TypedEventEmitter } from "./index";
interface ApiEventMap {
"API:LOGOUT": void;
"API:LOGIN": void;
}
/** 请求事件 */
const ApiEventEmitter = new TypedEventEmitter<ApiEventMap>();
export default ApiEventEmitter实际场景使用
- 当前假设 已经在全局注册了事件中心,并且已经引入了事件中心。并在axios中中使用了拦截器,拦截器中触发事件。
// axios.ts 中
// 响应拦截中
server.interceptors.response.use(
(res: any) => {
let { data, status } = res;
if (status === 301) {
// 登出事件需要做两件事清除store 和 跳转到login页面
// 一般写法肯定是引入路由写登出逻辑,但是如果不同的状态码,处理的逻辑不同,耦合度就太高了。
// 这里使用发布订阅解耦后就不用做任何处理,剩下的交给router.ts和store.ts处理事件
ApiEventEmitter.emit("API:LOGOUT");
}
return Promise.resolve(data);
},
(error: Error) => {
console.log("响应失败", error);
return Promise.resolve(error);
}
);- 路由需要跳转login页面
// router.ts 中
// 创建路由的文件示例
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 监听事件
ApiEventEmitter.on('API:LOGOUT', () => {
router.push('/login')
})
export default router;- 清理store
// 监听事件
ApiEventEmitter.on('API:LOGOUT', () => {
// 清除逻辑,可能还有更多逻辑
localStorer.clear()
sessionStore.clear()
....
})总结
- 现在其实可以发现,这种写法减少了axios请求拦截的处理逻辑,也降低了各个模块之间的耦合度。
- 各司其职,避免开发或者后期维护,改动更多模块。
- 建议使用TS来完成,拥有类型提示,可以在开发中更加清楚事件作用。
- 如果事件很多的话建议:一定要模块化,不要把所有事件都放在一个文件中,这样后期维护起来比较麻烦。
- 大型项目还是用中间件实现吧,支持更好。