在大型 UniApp 项目中,组件间通信通常依赖 uni.$on
和 uni.$emit
。
随着业务增长,我们遇到了越来越多的事件管理问题。本文分享事件管理器重构的整个过程,包括遇到的问题、优化思路和最终实现。
一、问题起点:事件监听器混乱
1. 独立注册,缺乏统一管理
原始代码中,每个组件都直接注册和触发事件:
uni.$on('initPageIndex', () => getRewards())
uni.$on('isRongCloudAndSocketInit', () => preLoadComponent())
uni.$emit('isRongCloudAndSocketInit')
问题:
事件名硬编码
- 拼写错误可能导致运行时异常
- 难以追踪哪些组件监听了哪些事件
缺乏类型检查
- 编译时无法检测事件名或参数类型
- 运行时错误难以调试
内存管理混乱
- 组件卸载时没有统一清理
$off
- 易造成内存泄漏
- 组件卸载时没有统一清理
这让我们意识到:必须先统一管理监听器,才能避免混乱。
二、第一步优化:统一事件枚举
为了减少硬编码,我们最初将所有事件放在一个枚举里:
export enum EventTypes {
INIT_PAGE_INDEX = 'initPageIndex',
RONGCLOUD_AND_SOCKET_INIT = 'isRongCloudAndSocketInit',
FOLLOW_ANCHOR = 'FOLLOW_ANCHOR',
BLOCK_ANCHOR = 'BLOCK_ANCHOR',
SOCKET_CONNECT = 'SOCKET_CONNECT',
SOCKET_DISCONNECT = 'SOCKET_DISCONNECT',
}
遇到的问题
平铺枚举,不直观
- 所有事件混在一起,难以判断事件所属模块
- 开发者需要记住前缀如
BROADCASTER_
或SOCKET_
维护成本高
- 项目增长时枚举快速臃肿
- 新增或删除事件容易出错
IDE 自动补全差
- 输入
EventTypes.
会显示所有事件 - 查找特定模块事件不方便
- 输入
平铺枚举解决了硬编码问题,但结构和可维护性仍然不足。
三、最终优化:多层模块化事件结构
1. 核心设计理念
- 直观性:一眼看出事件属于哪个模块
- 类型安全:保持完整 TypeScript 类型支持
- 可维护性:便于添加、删除、重构事件
- 向后兼容:平滑迁移,不破坏现有代码
2. 多层结构实现
export const EventTypes = {
// 初始化事件
init: {
PAGE_INDEX: 'initPageIndex',
RONGCLOUD_AND_SOCKET_INIT: 'isRongCloudAndSocketInit',
},
// 主播事件
broadcaster: {
FOLLOW_ANCHOR: 'FOLLOW_ANCHOR',
BLOCK_ANCHOR: 'BLOCK_ANCHOR',
MG_USER_LEFT: 'mgUserLeft',
},
// Socket事件
socket: {
CONNECT: 'SOCKET_CONNECT',
DISCONNECT: 'SOCKET_DISCONNECT',
MESSAGE_EVENT: 'messageEvent',
}
} as const
优势:
- 结构清晰,IDE 自动补全友好
- 易于维护:新增事件直接加到对应模块
- 减少拼写错误和逻辑混乱
3. 类型安全支持
export type EventType =
| typeof EventTypes.init[keyof typeof EventTypes.init]
| typeof EventTypes.broadcaster[keyof typeof EventTypes.broadcaster]
| typeof EventTypes.socket[keyof typeof EventTypes.socket]
export type EventData = {
[EventTypes.init.PAGE_INDEX]: null
[EventTypes.broadcaster.FOLLOW_ANCHOR]: { userId: string; type: 'follow' | 'unfollow' }
[EventTypes.socket.CONNECT]: null
}
- 联合类型 + 映射类型约束事件名和参数
- 编译期自动检查
$on/$emit
- IDE 补全提高开发体验
四、封装事件管理器
/**
* 统一事件管理器类
* 通过泛型参数区分不同类型的事件:Init、Broadcaster、Socket
*/
class EventManager {
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map()
/**
* 添加事件监听器,保证同一个 listener 不重复注册
*/
$on<T extends EventType>(eventType: T, listener: EventListeners[T]) {
if (!listener) return
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, new Set())
}
const listenerSet = this.listeners.get(eventType)!
if (!listenerSet.has(listener)) {
listenerSet.add(listener)
try {
uni.$on(eventType, listener)
} catch (e) {
console.warn(`uni.$on failed for event ${eventType}:`, e)
}
}
}
/**
* 派发事件,统一通过 uni.$emit
*/
$emit<T extends EventType>(eventType: T, data?: EventData[T]) {
try {
if (data !== undefined) {
uni.$emit(eventType, data)
} else {
uni.$emit(eventType)
}
} catch (e) {
console.warn(`uni.$emit failed for event ${eventType}:`, e)
}
}
/**
* 移除指定事件监听器
*/
$off<T extends EventType>(eventType: T, listener: EventListeners[T]) {
const listenerSet = this.listeners.get(eventType)
if (listenerSet) {
listenerSet.delete(listener)
}
try {
uni.$off(eventType, listener)
} catch (e) {
console.warn(`uni.$off failed for event ${eventType}:`, e)
}
}
/**
* 移除指定事件类型的所有监听器
*/
$offAll(eventType: EventType) {
const listenerSet = this.listeners.get(eventType)
if (listenerSet) {
listenerSet.forEach(listener => {
try {
uni.$off(eventType, listener)
} catch (e) {
console.warn(`uni.$off failed for event ${eventType}:`, e)
}
})
listenerSet.clear()
}
}
/**
* 按事件组清理监听器
*/
$clearGroup(group: keyof typeof EventGroups) {
const groupEvents = Object.values(EventGroups[group])
groupEvents.forEach(eventType => this.$offAll(eventType))
}
/**
* 清理所有事件监听器
*/
$clear() {
this.listeners.forEach((listenerSet, eventType) => {
listenerSet.forEach(listener => {
try {
uni.$off(eventType, listener)
} catch (e) {
console.warn(`uni.$off failed for event ${eventType}:`, e)
}
})
})
this.listeners.clear()
}
/**
* 获取指定事件类型的监听器数量
*/
getListenerCount(eventType: EventType): number {
return this.listeners.get(eventType)?.size || 0
}
/**
* 检查是否有指定事件类型的监听器
*/
hasListeners(eventType: EventType): boolean {
return this.getListenerCount(eventType) > 0
}
}
// 创建单例实例
const eventManager = new EventManager()
export default eventManager
优化点:
- 统一管理:所有事件集中操作
- 内存安全:组件卸载可清理监听器
- 类型安全:事件名和参数均可编译检查
五、使用示例
重构前
uni.$on('FOLLOW_ANCHOR', (data) => {
console.log('关注主播', data)
})
重构后
eventManager.$on(EventTypes.broadcaster.FOLLOW_ANCHOR, (data) => {
console.log('关注主播', data)
})
eventManager.$emit(EventTypes.broadcaster.FOLLOW_ANCHOR, { userId: '123', type: 'follow' })
onUnmounted(() => {
eventManager.$offAll(EventTypes.broadcaster.FOLLOW_ANCHOR)
})
优势:
- 模块化结构一目了然
- IDE 自动补全
- 编译期类型检查
- 内存清理机制完善
六、工程价值总结
核心优化点
- 从混乱到统一:解决监听器混乱和硬编码问题
- 从平铺到模块化:枚举分层,直观可维护
- 类型安全:TS 编译期检查,减少运行时错误
- 开发效率提升:IDE 自动补全清晰
- 内存安全:统一清理机制
设计原则
- 直观优于简洁:一眼看出事件所属模块
- 结构优于平铺:便于维护和扩展
- 类型安全:充分利用 TypeScript
- 向后兼容:渐进式迁移,不破坏原有代码
小封装,大价值。通过这次优化,事件管理器不仅解决了原有混乱问题,也为项目长期维护和扩展打下了坚实基础。