HarmonyOS 6.0 应用级事件总线与组件间通信
2026/7/27 14:05:11 网站建设 项目流程

组件通信有@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的使用场景很明确:

  1. 监听系统事件:屏幕开关、网络变化、时区变化、电量变化
  2. 跨应用通知:支付应用通知订单完成,地图应用接收导航请求
  3. 多进程通信:同一个应用的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。但当通信距离超出组件树范围时,事件总线就是最干净的解法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询