
文章目录前言什么时候该用事件总线类型安全的事件定义EventBus 核心实现组件自动订阅 / 取消订阅发送事件内存泄漏防护与调试实战首页 Tab 联动踩过的坑前言鸿蒙里组件通信父子之间用Prop/Link就行兄弟组件可以靠父组件中转。但碰到这种场景就难受了首页有个购物车角标在商品详情页点加入购物车后要实时更新这两个页面隔了好几层中间还可能有 Tab 切换。一层层传回调想想就头皮发麻。这时候事件总线就是最直接的方案——发一个事件谁关心谁订阅中间不需要任何传递。什么时候该用事件总线事件总线适合这些场景跨层级通知购物车数量变化、登录状态变更、主题切换跨 Tab 通信一个 Tab 的操作影响另一个 Tab 的数据全局状态变更网络状态切换在线/离线、权限变更不适合的场景父子组件通信用Prop/Link共享数据状态用 AppStorage 更合适请求响应模式一个事件期望一个返回值事件总线是单向通知发出去就不管了别拿它当 RPC 用。类型安全的事件定义用字符串做事件名太容易出错了拼错了编译器也不会报错。我用泛型把事件名和参数类型绑死// 事件定义事件名 → 参数类型的映射exportinterfaceEventMap{// 购物车cart:updated:{count:number;lastItemId:string};cart:cleared:void;// 用户user:login:{userId:string;username:string};user:logout:void;user:profile-updated:{field:string;value:string};// 网络network:status-changed:{online:boolean};// 主题theme:changed:{mode:light|dark};// 订单order:created:{orderId:string;totalAmount:number};order:cancelled:{orderId:string;reason:string};}// 事件回调函数类型exporttypeEventHandlerTTextendsvoid?()void:(payload:T)void;这样在注册和发送事件时TypeScript 会自动检查参数类型是否匹配拼错事件名直接报红。EventBus 核心实现interfaceSubscription{id:string;eventName:string;handler:Function;once:boolean;}exportclassEventBus{privatestaticinstance:EventBus;privatesubscriptions:Mapstring,Subscription[]newMap();privateidCounter:number0;privateconstructor(){}staticgetInstance():EventBus{if(!EventBus.instance){EventBus.instancenewEventBus();}returnEventBus.instance;}// 订阅事件onKextendskeyofEventMap(eventName:K,handler:EventHandlerEventMap[K]):string{constidsub_${this.idCounter};constsubscription:Subscription{id,eventName:eventNameasstring,handler:handlerasFunction,once:false};constlistthis.subscriptions.get(eventNameasstring)??[];list.push(subscription);this.subscriptions.set(eventNameasstring,list);returnid;}// 订阅一次性事件onceKextendskeyofEventMap(eventName:K,handler:EventHandlerEventMap[K]):string{constidsub_${this.idCounter};constsubscription:Subscription{id,eventName:eventNameasstring,handler:handlerasFunction,once:true};constlistthis.subscriptions.get(eventNameasstring)??[];list.push(subscription);this.subscriptions.set(eventNameasstring,list);returnid;}// 取消订阅off(subscriptionId:string):void{for(const[eventName,list]ofthis.subscriptions){constindexlist.findIndex(ss.idsubscriptionId);if(index!-1){list.splice(index,1);if(list.length0){this.subscriptions.delete(eventName);}return;}}}// 取消某个事件的所有订阅offAllKextendskeyofEventMap(eventName:K):void{this.subscriptions.delete(eventNameasstring);}// 发送事件emitKextendskeyofEventMap(eventName:K,...args:EventMap[K]extendsvoid?[]:[EventMap[K]]):void{constlistthis.subscriptions.get(eventNameasstring);if(!list||list.length0)return;constpayloadargs[0];consttoRemove:string[][];for(constsuboflist){try{if(payload!undefined){sub.handler(payload);}else{sub.handler();}}catch(e){Logger.error(EventBus,事件 ${eventNameasstring} 处理器出错,{error:String(e),subscriptionId:sub.id});}if(sub.once){toRemove.push(sub.id);}}// 清理一次性订阅for(constidoftoRemove){this.off(id);}}// 查看当前所有订阅调试用debugInfo():Recordstring,number{constinfo:Recordstring,number{};for(const[eventName,list]ofthis.subscriptions){info[eventName]list.length;}returninfo;}}// 全局单例exportconsteventBusEventBus.getInstance();组件自动订阅 / 取消订阅内存泄漏是事件总线最大的坑。组件销毁了但订阅还在每次事件触发都会执行一个已经无效的回调时间长了内存和性能都扛不住。我封装了一个 mixin 风格的基类自动在组件生命周期里管理订阅// 事件订阅管理器在组件中使用exportclassEventSubscriber{privatesubscriptionIds:string[][];// 订阅事件自动记录 IDonKextendskeyofEventMap(eventName:K,handler:EventHandlerEventMap[K]):void{constideventBus.on(eventName,handler);this.subscriptionIds.push(id);}// 订阅一次性事件onceKextendskeyofEventMap(eventName:K,handler:EventHandlerEventMap[K]):void{constideventBus.once(eventName,handler);this.subscriptionIds.push(id);}// 取消所有订阅unsubscribeAll():void{for(constidofthis.subscriptionIds){eventBus.off(id);}this.subscriptionIds[];Logger.debug(EventSubscriber,已清理${this.subscriptionIds.length}个订阅);}}在组件中使用aboutToAppear里订阅aboutToDisappear里清理Componentstruct CartBadge{Statecount:number0;privatesubscribernewEventSubscriber();aboutToAppear():void{// 订阅购物车更新事件this.subscriber.on(cart:updated,(payload){this.countpayload.count;Logger.info(CartBadge,购物车数量更新:${payload.count});});// 订阅购物车清空事件this.subscriber.on(cart:cleared,(){this.count0;});}aboutToDisappear():void{// 组件销毁时自动清理所有订阅this.subscriber.unsubscribeAll();}build(){if(this.count0){Text(${this.count}).fontSize(12).fontColor(Color.White).backgroundColor(#FF4444).borderRadius(10).padding({left:6,right:6,top:2,bottom:2})}}}发送事件发送事件的地方通常是在 Service 或者用户交互的回调里// 在 ProductService 中加入购物车asyncfunctionaddToCart(productId:string):Promisevoid{constresultawaithttpService.post{count:number}(/cart/add,{productId});// 发出事件CartBadge 和其他关心的组件会自动收到eventBus.emit(cart:updated,{count:result.count,lastItemId:productId});}// 用户退出登录时asyncfunctionlogout():Promisevoid{awaithttpService.post(/auth/logout);// 发出多个事件eventBus.emit(user:logout);eventBus.emit(cart:cleared);}内存泄漏防护与调试除了自动清理我还加了两个防护机制// 1. 弱引用检测定期输出订阅数量发现泄漏exportfunctionstartLeakDetection(intervalMs:number30000):void{setInterval((){constinfoeventBus.debugInfo();consttotalSubsObject.values(info).reduce((sum,n)sumn,0);if(totalSubs100){Logger.warn(EventBus,⚠️ 订阅数量过多:${totalSubs}可能存在泄漏,{detail:info}asRecordstring,Object);}else{Logger.debug(EventBus,当前订阅数:${totalSubs},{detail:info}asRecordstring,Object);}},intervalMs);}// 2. 组件关联订阅把订阅和组件 ID 绑定方便排查exportclassTrackedEventSubscriber{privatesubscribernewEventSubscriber();privatecomponentId:string;constructor(componentId:string){this.componentIdcomponentId;}onKextendskeyofEventMap(eventName:K,handler:EventHandlerEventMap[K]):void{Logger.debug(EventBus,[${this.componentId}] 订阅${eventNameasstring});this.subscriber.on(eventName,handler);}unsubscribeAll():void{Logger.debug(EventBus,[${this.componentId}] 取消所有订阅);this.subscriber.unsubscribeAll();}}实战首页 Tab 联动一个完整的例子展示事件总线怎么在多个 Tab 之间做联动// 首页 TabComponentstruct HomeTab{StatenewOrderCount:number0;privatesubscribernewEventSubscriber();aboutToAppear():void{this.subscriber.on(order:created,(payload){this.newOrderCount;// 显示新订单提示promptAction.showToast({message:新订单: ¥${payload.totalAmount}});});}aboutToDisappear():void{this.subscriber.unsubscribeAll();}build(){Column(){if(this.newOrderCount0){Badge({count:this.newOrderCount}){Text(新订单)}}// ... 首页内容}}}// 订单 TabComponentstruct OrderTab{privatesubscribernewEventSubscriber();aboutToAppear():void{// 监听购物车清空刷新订单列表this.subscriber.on(cart:cleared,(){this.refreshOrders();});}aboutToDisappear():void{this.subscriber.unsubscribeAll();}asyncrefreshOrders(){// 刷新逻辑}build(){// ... 订单列表}}踩过的坑忘记取消订阅是最常见的内存泄漏原因。尤其是动态创建的组件比如LazyForEach里的 item创建和销毁频繁如果每个 item 都订阅了事件但不取消订阅数量会爆炸式增长。事件处理器里不要做重操作。事件是同步触发的如果一个事件有 10 个订阅者每个都做了网络请求那瞬间就是 10 个并发请求。重操作应该放到异步队列里。别用事件总线替代 AppStorage。如果只是共享一个数据比如用户信息AppStorage 更合适。事件总线适合通知而不是共享状态。类型定义要维护好。EventMap接口是整个事件系统的核心契约新增事件一定要在里面定义别偷懒直接用字符串。否则半年后回来改代码根本不知道哪个事件传了什么参数。