HarmonyOS提供Emitter和CommonEvent两套事件总线方案。Emitter用于应用内任意组件解耦通信,支持一对多广播;CommonEvent用于跨应用系统事件通知。需注意Emitter在组件销毁时取消订阅防止内存泄漏,CommonEvent需声明权限且不保证事件顺序。应用内优先使用Emitter,避免CommonEvent的跨进程开销。
在HarmonyOS组件通信中,大家首先想到的通常是@Prop/@Link、@Provide/@Consume、@Watch这些“正规军”。然而,面对跨Ability通信、非父子组件通信、一对多广播、解耦通信等场景,这些方案确实难以胜任。此时,事件总线便成为关键工具。HarmonyOS提供了Emitter和CommonEvent两套方案,分别用于应用内和应用间通信,但许多开发者难以区分两者的适用场景。
| 方案 | 通信范围 | 耦合度 | 适用场景 |
|---|---|---|---|
| @Prop/@Link | 父子组件 | 高 | 父子数据同步 |
| @Provide/@Consume | 跨层级 | 中 | 祖孙组件通信 |
| AppStorage | 全局 | 中 | 全局状态共享 |
| Emitter | 应用内任意 | 低 | 组件间解耦通信 |
| CommonEvent | 跨应用 | 低 | 系统事件、跨应用通知 |
Emitter是应用内的事件总线,CommonEvent是系统级的事件广播。 大多数场景使用Emitter即可满足需求,CommonEvent仅在需要与系统或其他应用通信时使用。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
Emitter是@kit.BasicServicesKit提供的进程内事件机制,用于HarmonyOS应用内的事件总线通信:
import { emitter } from '@kit.BasicServicesKit';
// 定义事件ID
const EVENT_LOGIN_SUCCESS = 1001;
const EVENT_CART_UPDATED = 1002;
// 发送事件
emitter.emit({
eventId: EVENT_LOGIN_SUCCESS,
parameters: { userId: '12345', userName: '张三' }
});
// 接收事件
emitter.on({
eventId: EVENT_LOGIN_SUCCESS
}, (eventData: emitter.EventData) => {
let params = eventData.parameters;
let userId = params['userId'] as string;
this.onLoginSuccess(userId);
});
eventId是数字类型的事件标识,需自行定义。parameters用于携带数据,类型为Record
关键:Emitter是进程内的,仅在同一应用内才能收发。 跨进程(不同Ability运行在不同进程)时Emitter不生效。
// 订阅时保存回调引用
private loginCallback = (eventData: emitter.EventData) => {
// 处理
}
aboutToAppear(): void {
emitter.on({ eventId: EVENT_LOGIN_SUCCESS }, this.loginCallback);
}
aboutToDisappear(): void {
emitter.off(EVENT_LOGIN_SUCCESS, this.loginCallback);
}
必须在aboutToDisappear中调用off取消订阅,否则组件销毁后回调仍会执行,导致内存泄漏和空引用crash。
off必须传入与on相同的回调函数引用。因此回调不能写成匿名函数,需保存为类成员变量。
Emitter天然支持一对多广播——多个组件订阅同一个eventId,emit时所有订阅者都会收到通知:
// 购物车页面
emitter.emit({ eventId: EVENT_CART_UPDATED, parameters: { count: 5 } });
// TabBar组件
emitter.on({ eventId: EVENT_CART_UPDATED }, (data) => {
this.cartBadge = data.parameters['count'] as number;
});
// 首页组件
emitter.on({ eventId: EVENT_CART_UPDATED }, (data) => {
this.refreshRecommendations();
});
// 消息中心
emitter.on({ eventId: EVENT_CART_UPDATED }, (data) => {
this.checkPromotions();
});
三个组件同时监听购物车更新事件,emit一次,三个组件均能收到。这是@Prop/@Link无法实现的——它们要求组件之间存在明确的层级关系。
经典场景:登录成功后,多个页面需要同步更新状态:
const EVENT_USER_STATE_CHANGED = 2001; // 登录页 async login(username: string, password: string): Promise{ let user = await this.authService.login(username, password); AppStorage.setOrCreate('currentUser', user); emitter.emit({ eventId: EVENT_USER_STATE_CHANGED, parameters: { isLogin: true, userId: user.id } }); } // 退出登录 logout(): void { this.authService.logout(); AppStorage.delete('currentUser'); emitter.emit({ eventId: EVENT_USER_STATE_CHANGED, parameters: { isLogin: false } }); } // 个人中心页 aboutToAppear(): void { emitter.on({ eventId: EVENT_USER_STATE_CHANGED }, this.onUserStateChanged); } private onUserStateChanged = (data: emitter.EventData) => { let isLogin = data.parameters['isLogin'] as boolean; if (isLogin) { this.showUserInfo(); } else { this.showLoginButton(); } }
为什么不直接用AppStorage? AppStorage的数据变化会触发UI刷新,但不会触发逻辑回调。例如退出登录时,不仅需要更新UI,还需清理缓存、取消请求、重置状态等业务逻辑。Emitter可以触发这些逻辑回调,实现更精细的控制。
直接使用emitter的API略显繁琐。可以封装一个类型安全的事件总线,提升开发效率:
class EventBus {
private static instance: EventBus;
private handlers: Map = new Map();
static getInstance(): EventBus {
if (!EventBus.instance) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
on(eventId: number, callback: emitter.Callback): void {
let existing = this.handlers.get(eventId);
if (existing === undefined) {
existing = [];
}
existing.push(callback);
this.handlers.set(eventId, existing);
emitter.on({ eventId: eventId }, callback);
}
off(eventId: number, callback: emitter.Callback): void {
let existing = this.handlers.get(eventId);
if (existing !== undefined) {
let newList: emitter.Callback[] = [];
for (let i = 0; i < existing.length; i++) {
if (existing[i] !== callback) {
newList.push(existing[i]);
}
}
this.handlers.set(eventId, newList);
}
emitter.off(eventId, callback);
}
emit(eventId: number, parameters: Record): void {
if (parameters !== undefined) {
emitter.emit({ eventId: eventId, parameters: parameters });
} else {
emitter.emit({ eventId: eventId });
}
}
}
封装后使用更简洁:
EventBus.getInstance().on(EVENT_LOGIN, this.onLogin);
EventBus.getInstance().emit(EVENT_LOGIN, { userId: '123' });
EventBus.getInstance().off(EVENT_LOGIN, this.onLogin);
CommonEvent是系统级广播,支持跨应用、跨进程通信:
import { commonEventManager } from '@kit.BasicServicesKit';
// 订阅系统事件
let subscriber = commonEventManager.createSubscriber({
events: ['usual.event.SCREEN_OFF']
});
commonEventManager.subscribe(subscriber, (err, data) => {
// 处理屏幕关闭事件
});
// 取消订阅
commonEventManager.unsubscribe(subscriber);
CommonEvent需要权限声明。 许多系统事件需要对应权限才能订阅。自定义事件无需权限,但需确保事件名的唯一性——建议使用包名作为前缀:
let CUSTOM_EVENT = 'com.example.app.ORDER_CREATED';
发送自定义CommonEvent的示例:
commonEventManager.publish('com.example.app.ORDER_CREATED', {
parameters: {
orderId: '20240101001',
amount: 99.9
}
}, (err) => {
if (!err) {
console.info('Event published');
}
});
publish是异步操作,回调在发送完成后触发。
注意:CommonEvent的发送和接收可以在不同应用中。 这意味着事件可能被其他应用监听到,敏感数据不应直接放在parameters中。
CommonEvent的使用场景非常明确:
应用内通信不应使用CommonEvent——它比Emitter更重(涉及跨进程序列化),且存在权限和可见性问题。
事件到达顺序不保证。如果A事件必须在B事件之前处理,可以采用事件链:
const EVENT_STEP1_COMPLETE = 3001;
// A完成后发事件
emitter.emit({ eventId: EVENT_STEP1_COMPLETE });
// B等A完成才执行
emitter.on({ eventId: EVENT_STEP1_COMPLETE }, () => {
this.executeStep2();
});
用事件链代替时序假设——A完成后emit通知,B监听通知再执行。不要假设emit是同步的。
Emitter的回调如果不取消订阅,组件销毁后依然会执行。最危险的情况:
// 危险:匿名函数无法off
emitter.on({ eventId: 1001 }, (data) => {
this.updateUI(); // 组件已销毁,this可能无效
});
// 安全:保存引用,组件销毁时off
private handler = (data: emitter.EventData) => {
this.updateUI();
}
aboutToAppear(): void {
emitter.on({ eventId: 1001 }, this.handler);
}
aboutToDisappear(): void {
emitter.off(1001, this.handler);
}
规则:on和off必须成对出现,回调必须是类成员变量,不能是匿名函数。
| 问题 | 原因 | 解决 |
|---|---|---|
| 组件销毁后crash | 没off取消订阅 | aboutToDisappear中off |
| off不掉 | 匿名函数无法比较引用 | 回调保存为类成员变量 |
| 事件收不到 | Emitter跨进程不生效 | 跨进程用CommonEvent |
| 收到重复事件 | 多次on同一回调 | on前先off |
| 事件顺序不对 | emit不保证同步时序 | 用事件链传递顺序 |
| CommonEvent收不到 | 没声明权限 | 检查module.json5权限 |
| 自定义事件被拦截 | 事件名不够唯一 | 用包名作前缀 |
| 参数类型丢失 | parameters反序列化 | 只传基本类型 |
| 数据敏感泄露 | CommonEvent跨应用可见 | 敏感数据加密或用Emitter |
| 事件总线内存增长 | Map中回调越积越多 | off时清理Map记录 |
事件总线是组件通信的“后门”——不用它时架构清晰,用了它时调用链隐晦。能用@Prop/@Link解决的问题不要使用Emitter,能不跨进程的通信不要使用CommonEvent。但当通信距离超出组件树范围时,事件总线就是最干净的解法。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述