HarmonyOS 6.0 应用级事件总线与组件间通信
组件通信有@Prop/@Link、@Provide/@Consume、@Watch这些"正规军"。但有些场景它们搞不定——跨Ability通信、非父子组件通信、一对多广播、解耦通信。这时候就得用事件总线。HarmonyOS提供了Emitter和CommonEvent两套方案,分别管应用内和应用间,但很多人分不清什么时候用哪个。
通信方案对比
| 方案 | 通信范围 | 耦合度 | 适用场景 |
|---|---|---|---|
| @Prop/@Link | 父子组件 | 高 | 父子数据同步 |
| @Provide/@Consume | 跨层级 | 中 | 祖孙组件通信 |
| AppStorage | 全局 | 中 | 全局状态共享 |
| Emitter | 应用内任意 | 低 | 组件间解耦通信 |
| CommonEvent | 跨应用 | 低 | 系统事件、跨应用通知 |
Emitter是应用内的事件总线,CommonEvent是系统级的事件广播。 大多数场景用Emitter就够了,CommonEvent只在需要跟系统或其他应用通信时才用。
Emitter基础
Emitter是@kit.BasicServicesKit提供的进程内事件机制:
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<string, Object>。
关键:Emitter是进程内的,同一个应用内才能收发。 跨进程(不同Ability运行在不同进程)时Emitter不生效。
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<void> {
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,还要清理缓存、取消请求、重置状态——这些是业务逻辑,不是UI渲染。Emitter可以触发这些逻辑回调。
封装事件总线
直接用emitter的API有点啰嗦。封装一个类型安全的事件总线:
class EventBus {
private static instance: EventBus;
private handlers: Map<number, emitter.Callback[]> = 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<string, Object>): 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跨应用通信
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发送
发送自定义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的使用场景很明确:
- 监听系统事件:屏幕开关、网络变化、时区变化、电量变化
- 跨应用通知:支付应用通知订单完成,地图应用接收导航请求
- 多进程通信:同一个应用的UIAbility和ExtensionAbility在不同进程
应用内通信不要用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,组件销毁后依然执行。最危险的情况:
// 危险:匿名函数无法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。但当通信距离超出组件树范围时,事件总线就是最干净的解法。
更多推荐


所有评论(0)