组件通信有@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';// 定义事件IDconstEVENT_LOGIN_SUCCESS=1001;constEVENT_CART_UPDATED=1002;// 发送事件emitter.emit({eventId:EVENT_LOGIN_SUCCESS,parameters:{userId:'12345',userName:'张三'}});// 接收事件emitter.on({eventId:EVENT_LOGIN_SUCCESS},(eventData:emitter.EventData)=>{letparams=eventData.parameters;letuserId=params['userId']asstring;this.onLoginSuccess(userId);});eventId是数字类型的事件标识,自己定义。parameters携带数据,类型是Record<string, Object>。
关键:Emitter是进程内的,同一个应用内才能收发。跨进程(不同Ability运行在不同进程)时Emitter不生效。
Emitter取消订阅
// 订阅时保存回调引用privateloginCallback=(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']asnumber;});// 首页组件emitter.on({eventId:EVENT_CART_UPDATED},(data)=>{this.refreshRecommendations();});// 消息中心emitter.on({eventId:EVENT_CART_UPDATED},(data)=>{this.checkPromotions();});三个组件同时监听购物车更新事件,emit一次,三个都收到。这是@Prop/@Link做不到的——它们需要组件有明确的层级关系。
登录状态同步
经典场景:登录成功后,多个页面需要更新状态:
constEVENT_USER_STATE_CHANGED=2001;// 登录页asynclogin(username:string,password:string):Promise<void>{letuser=awaitthis.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);}privateonUserStateChanged=(data:emitter.EventData)=>{letisLogin=data.parameters['isLogin']asboolean;if(isLogin){this.showUserInfo();}else{this.showLoginButton();}}为什么不直接用AppStorage?AppStorage的数据变化会触发UI刷新,但不会触发逻辑回调。比如退出登录时你不仅要更新UI,还要清理缓存、取消请求、重置状态——这些是业务逻辑,不是UI渲染。Emitter可以触发这些逻辑回调。
封装事件总线
直接用emitter的API有点啰嗦。封装一个类型安全的事件总线:
classEventBus{privatestaticinstance:EventBus;privatehandlers:Map<number,emitter.Callback[]>=newMap();staticgetInstance():EventBus{if(!EventBus.instance){EventBus.instance=newEventBus();}returnEventBus.instance;}on(eventId:number,callback:emitter.Callback):void{letexisting=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{letexisting=this.handlers.get(eventId);if(existing!==undefined){letnewList:emitter.Callback[]=[];for(leti=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';// 订阅系统事件letsubscriber=commonEventManager.createSubscriber({events:['usual.event.SCREEN_OFF']});commonEventManager.subscribe(subscriber,(err,data)=>{// 处理屏幕关闭事件});// 取消订阅commonEventManager.unsubscribe(subscriber);CommonEvent需要权限声明。很多系统事件需要对应权限才能订阅。自定义事件不需要权限,但要确保事件名的唯一性——建议用包名作前缀:
letCUSTOM_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事件之前处理:
constEVENT_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,组件销毁后依然执行。最危险的情况:
// 危险:匿名函数无法offemitter.on({eventId:1001},(data)=>{this.updateUI();// 组件已销毁,this可能无效});// 安全:保存引用,组件销毁时offprivatehandler=(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。但当通信距离超出组件树范围时,事件总线就是最干净的解法。