鸿蒙新特性实战:@ohos.events.emitter — 进程内事件总线与隔离通道
引言在第 36 篇文章中我们深入探讨了ohos.commonEventManager——HarmonyOS 的系统级跨进程事件通信机制。但并非所有事件都需要跨越进程边界。在 UI 组件和业务逻辑的解耦、Model 层的数据变更通知、插件系统的消息分发等场景中进程内事件总线是更轻量、更高效的选择。HarmonyOS NEXT 通过ohos.events.emitter模块提供了完整的进程内事件系统。与 Node.js 的EventEmitter或 Android 的LiveData/EventBus类似它允许应用内部的不同模块通过事件 ID 进行松耦合通信。更为新颖的是API 22 引入的Emitter类支持创建多个完全隔离的事件通道实例——每个实例拥有独立的事件-监听器映射表同一事件 ID 在不同 Emitter 实例上互不干扰。本文将构建一个进程内通信实验室Demo从全局 emitter API 和 Emitter 实例两个维度演示事件订阅on/once、事件发送emit 优先级、通道隔离以及通过优先级竞赛验证事件投递的调度逻辑。读完本文你将掌握持久订阅 vs 一次性订阅on()持续接收 vsonce()触发后自动注销事件优先级调度EventPriorityIMMEDIATE / HIGH / LOW / IDLE的执行顺序带数据事件EventData的data字段传递任意键值对监听者计数getListenerCount()运行时查询活跃监听数隔离通道new Emitter()创建独立事件通道API 22 新特性全局 vs 实例对比何时用全局 emitter何时用 Emitter 类环境与权限ohos.events.emitter属于BasicServicesKit自 API 7 开始提供Emitter 类自 API 22 起。导入方式importemitterfromohos.events.emitter;零权限需求整个emitter模块完全运行在进程内不涉及任何系统服务调用因此不需要任何权限声明。这是它相比commonEventManager的另一个优势——开箱即用零配置。一、核心数据类型1.1 EventData — 事件载荷interfaceEventData{data?:{[key:string]:any};}EventData是事件携带数据的容器。data是一个可选的键值对对象可以传递任意 JSON 可序列化数据。emitter不做序列化——数据在进程内存中直接传递因此可以携带比commonEventManager限制 64KB 字符串更丰富的数据结构。1.2 InnerEvent — 内事件对象interfaceInnerEvent{eventId:number;// 事件 ID数字类型priority?:EventPriority;// 优先级发送时有效订阅时忽略}InnerEvent是早期 APIAPI 7的事件描述方式使用数字 ID。现代代码推荐使用字符串eventId的形式可读性更好。priority仅在emit()时生效——订阅时的priority参数被忽略。1.3 EventPriority — 优先级枚举enumEventPriority{IMMEDIATE0,// 立即执行最高优先级HIGH,// 高优先级LOW,// 低优先级默认IDLE// 空闲时执行最低优先级}事件优先级决定了同一eventId的多个监听器的执行顺序。注意优先级是对发送方而言的——高优先级的事件先投递给所有匹配的监听器。这与commonEventManager的有序事件按订阅者优先级串行传播是不同的模型。1.4 Options — 发送选项interfaceOptions{priority?:EventPriority;// 事件发送优先级}Options配合emit(eventId, options, data)使用用于指定本次发送的优先级。如果不传Options默认使用EventPriority.LOW。1.5 GenericEventDataT — 泛型数据interfaceGenericEventDataT{data?:T;}API 12 引入的泛型版本允许携带强类型数据。对于 TypeScript 项目这提供了编译期的类型检查interfaceOrderEvent{orderId:string;amount:number;}emitter.onOrderEvent(order_paid,(data){// data.data 类型为 OrderEvent | undefinedconsole.log(data.data?.orderId);});二、全局 Emitter API全局 emitter 是模块默认导出的单例对象提供了最常用的事件操作函数。2.1 持久订阅 — on()emitter.on(counter_changed,(data:emitter.EventData){console.log(计数器变化: JSON.stringify(data.data));});on()注册一个持久的事件监听器——每次匹配事件被emit()触发时回调都会执行。这是最常用的订阅方式适用于需要持续观察数据的场景。2.2 一次性订阅 — once()emitter.once(app_ready,(data:emitter.EventData){console.log(应用就绪执行一次性初始化);});once()注册一个一次性的监听器——当事件首次触发后该监听器自动被移除。这非常适合需要等待某个条件达成后执行单次操作如初始化完成、首屏渲染完毕的场景。关键差异如果通过on()和once()在同一个eventId上各注册了一个监听器发送一次事件后on()的回调执行且仍然保持活跃once()的回调执行然后立即注销再次发送事件时只有on()的回调会执行2.3 取消订阅 — off()// 方式一取消该 eventId 的所有监听器emitter.off(counter_changed);// 方式二取消该 eventId 的指定回调emitter.off(counter_changed,myCallback);off()有两种重载形式只传eventId清除该事件的所有监听器传eventIdcallback仅清除指定的那个监听器要求callback是同一个函数引用注意一旦用off(eventId)清除了某事件的所有监听器之前的on()和once()注册全部失效。在页面aboutToDisappear()生命周期中应调用off()避免回调在已销毁组件上执行。2.4 发送事件 — emit()// 基本发送默认 LOW 优先级emitter.emit(counter_changed,{data:{value:42}});// 指定优先级发送emitter.emit(counter_changed,{priority:emitter.EventPriority.IMMEDIATE},{data:{value:42}});emit()有三个重载emit(eventId, data)— 默认优先级emit(eventId, options, data)— 指定优先级emitT(eventId, options, data)— 泛型版本API 12优先级影响多个监听器的执行次序IMMEDIATE 先于 HIGH 先于 LOW 先于 IDLE。2.5 查询监听数 — getListenerCount()letcountemitter.getListenerCount(counter_changed);console.log(当前活跃监听器: count);getListenerCount()返回指定eventId的当前活跃监听器数量包括on()和once()注册的。这在运行时诊断中非常有用——可以验证监听器是否成功注册或者检测是否存在因off()未调用导致的累积。三、Emitter 类隔离事件通道API 22API 22 引入的Emitter类是emitter模块最具辨识度的新特性。每个Emitter实例维护自己独立的事件-监听器映射表不同实例之间的同名事件完全隔离。3.1 创建实例letchannelAnewemitter.Emitter();letchannelBnewemitter.Emitter();3.2 通道隔离演示// 通道 A 订阅 data_syncchannelA.on(data_sync,(data:emitter.EventData){console.log([A] 收到: JSON.stringify(data.data));});// 通道 B 也订阅 data_syncchannelB.on(data_sync,(data:emitter.EventData){console.log([B] 收到: JSON.stringify(data.data));});// 通过通道 A 发送channelA.emit(data_sync,{data:{from:A}});// 输出: [A] 收到: {from:A}// 注意[B] 没有任何输出因为两个通道完全隔离// 通过通道 B 发送channelB.emit(data_sync,{data:{from:B}});// 输出: [B] 收到: {from:B}// [A] 没有任何输出这个特性把命名空间冲突问题从设计层面彻底消除了。在使用全局 emitter 时不同模块必须小心选择不冲突的事件 ID而使用Emitter实例每个模块可以创建自己的实例在实例内部自由命名事件。3.3 使用场景对比场景推荐方案原因全局状态变更用户登录/登出全局 emitter需要所有模块都能感知页面内组件通信Emitter 实例页面销毁时实例随之一同清理插件系统每个插件一个 Emitter 实例不同插件的事件互不干扰数据管道生产者-消费者Emitter 实例多管道独立运行不串扰3.4 实例方法签名Emitter类的方法签名与全局 emitter 高度一致classEmitter{on(eventId:string,callback:CallbackEventData):void;once(eventId:string,callback:CallbackEventData):void;off(eventId:string):void;off(eventId:string,callback:CallbackEventData):void;emit(eventId:string,data?:EventData):void;emit(eventId:string,options:Options,data?:EventData):void;getListenerCount(eventId:string):number;}差异在于Emitter实例只支持字符串eventId不支持InnerEvent数字 IDEmitter实例没有泛型onT/onceT的单独声明但通过方法重载支持Emitter实例不提供全局 emitter 的emit(eventId, options, data)旧版 APIInnerEvent 模式四、优先级调度机制emitter 的优先级调度是在发送侧生效的。当多个事件都在线程队列中等待处理时优先级高的事件先被投递。这与 commonEventManager 在订阅侧按优先级串行传播的模型不同。4.1 优先级竞赛实验letexecOrder:string[][];emitter.once(race,(data){execOrder.push(IMMEDIATE);});emitter.once(race,(data){execOrder.push(HIGH);});emitter.once(race,(data){execOrder.push(LOW);});emitter.once(race,(data){execOrder.push(IDLE);});// 逆序发送——观察执行结果是否按优先级重排emitter.emit(race,{priority:emitter.EventPriority.IDLE});emitter.emit(race,{priority:emitter.EventPriority.LOW});emitter.emit(race,{priority:emitter.EventPriority.HIGH});emitter.emit(race,{priority:emitter.EventPriority.IMMEDIATE});// execOrder [IMMEDIATE, HIGH, LOW, IDLE]实验结果无论 emit 的调用顺序如何监听器的执行顺序始终是IMMEDIATE → HIGH → LOW → IDLE。这是因为优先级决定了事件在队列中的插入位置而非调用顺序。4.2 优先级使用指南IMMEDIATE需要在当前帧完成的关键更新如 UI 状态同步HIGH重要的业务逻辑回调如数据验证结果通知LOW默认优先级适用于大多数普通事件IDLE可延迟的非关键更新如统计上报、缓存写入五、实战 Demo进程内通信实验室页面结构进程内通信实验室 ├── 状态栏 — 当前状态 监听者计数 ├── 全局事件总线面板 │ ├── 事件 ID 输入框 数据输入框 │ ├── 优先级选择器IMMEDIATE/HIGH/LOW/IDLE │ ├── ON 订阅 / ONCE 订阅 / 发送 / 取消 按钮 │ └── ON 累计 / ONCE 累计 计数 ├── 优先级竞赛 │ ├── 说明文字 │ └── 运行竞赛按钮 ├── 隔离通道Emitter 实例 — API 22 │ ├── 创建 / 销毁 实例按钮 │ ├── A→ch_a / B→ch_b 发送按钮 │ └── 跨通道隔离测试A→ch_shared / B→ch_shared ├── 事件日志 — 最近 50 条 └── 核心 API 参考 — 10 个关键 API4 个交互点全局 Emitter 订阅与发送— 输入事件 ID 和数据选择 ON 持久订阅或 ONCE 一次性订阅选择优先级发送事件在日志中观察接收确认优先级竞赛— 点击运行优先级竞赛4 个 ONCE 监听器分别以 IMMEDIATE/HIGH/LOW/IDLE 优先级发送日志中输出实际执行顺序隔离通道Emitter 实例— 创建两个 Emitter 实例分别订阅同名事件通过各自实例发送验证事件完全隔离跨通道隔离验证— A 和 B 各自订阅ch_shared通过各自实例发送后仅各自的监听器触发互不干扰核心代码位置完整代码在dev/entry/src/main/ets/pages/EmitterLabPage.ets约 350 行路由已注册为pages/EmitterLabPage。六、emitter vs commonEventManager 对比第 36 篇文章介绍了ohos.commonEventManager它与ohos.events.emitter是 HarmonyOS 事件通信体系的两个支柱维度emittercommonEventManager通信范围进程内跨进程系统级底层机制内存事件表系统 Common Event Service权限需求无无自定义事件事件 ID 类型string 或 number仅 string优先级模型发送侧排序订阅侧串行传播一次性订阅once()不支持需手动 unsubscribe粘性事件不支持支持需权限隔离通道Emitter 类API 22不支持系统事件不支持Support 枚举30适用场景组件解耦、页面内通信跨应用通信、系统事件监听选择原则如果你的事件只需要在应用进程内传播——组件间通信、ViewModel 通知、数据变更回调——用emitter如果需要跨应用通信或监听系统底层状态变化用commonEventManager。七、与 Node.js EventEmitter 的对比对于有 Node.js 经验的开发者emitter的设计取向上值得对比维度Node.js EventEmitterHarmonyOS emitter事件 IDstring | Symbolstring | number优先级不支持EventPriority4 级一次性订阅once() — 行为一致once() — 行为一致监听者计数listenerCount()getListenerCount()隔离实例new EventEmitter()new emitter.Emitter()最大监听数setMaxListeners()无限制泛型支持社区实现原生支持API 12Node.js 的 EventEmitter 是 JavaScript 生态中最经典的事件系统emitter在核心 API 设计上与之保持了一致性on/once/off/emit同时增加了事件优先级和原生泛型支持。八、最佳实践与注意事项8.1 内存泄漏防范aboutToDisappear():void{emitter.off(this.eventId);// 清理全局订阅if(this.channel!null){this.channel.off(this.eventId);// 清理实例订阅this.channelnull;}}on()注册的持久监听器会阻止回调闭包被垃圾回收。页面或组件销毁前务必off()。8.2 事件 ID 命名约定推荐使用模块:动作的命名格式user:login/user:logout— 用户模块事件cart:add/cart:remove— 购物车事件order:paid/order:shipped— 订单状态事件使用一致的命名约定可以降低事件 ID 冲突的概率特别是在使用全局 emitter 时。8.3 Emitter 实例 vs 全局 emitter全局 emitter适合应用级事件用户登录、主题切换、语言变更Emitter 实例适合页面级事件表单值变更、列表项交互、功能模块内部通信一个典型的页面架构EntryComponentstruct OrderPage{privatechannel:emitter.Emitternewemitter.Emitter();aboutToAppear():void{this.channel.on(form:validate,(data){// 表单验证逻辑});}aboutToDisappear():void{this.channel.off(form:validate);// 页面销毁时清理}}这样设计的好处是页面销毁时实例上的所有监听器随对象一起被回收不会有残留监听器污染全局事件空间。8.4 避免过度使用事件总线是双刃剑——过度使用会导致代码中的数据流难以追踪。对于直接的父子组件通信优先使用Link/Prop/Provide/Consume对于跨页面的简单数据传递使用路由参数。只有在需要一对多、多对多的松耦合通信时才引入 emitter。九、API 总结表API返回值说明emitter.on(eventId, callback)void持久订阅全局事件emitter.once(eventId, callback)void一次性订阅全局事件emitter.off(eventId)void取消所有该事件监听emitter.off(eventId, callback)void取消指定回调监听emitter.emit(eventId, data)void发送事件默认优先级emitter.emit(eventId, options, data)void发送事件指定优先级emitter.getListenerCount(eventId)number查询活跃监听数new emitter.Emitter()Emitter创建隔离通道API 22Emitter.on/once/off/emit同全局实例方法API 22EventPriorityenumIMMEDIATE/HIGH/LOW/IDLE十、总结ohos.events.emitter是 HarmonyOS NEXT 的进程内事件通信基础设施核心要点零权限零配置完全运行在进程内存中无任何额外开销双重订阅模型on()持久 once()一次性精准匹配不同场景需求四级优先级调度发送侧的 IMMEDIATE → HIGH → LOW → IDLE 排序决定了监听器执行顺序监听者可见性getListenerCount()提供了运行时诊断能力隔离通道架构API 22 的Emitter类从根本上解决了全局事件空间的命名冲突问题结合ohos.commonEventManager跨进程系统事件和ohos.events.emitter进程内事件HarmonyOS 开发者已经拥有了从进程内到跨进程的完整事件通信工具箱。选择哪个模块取决于通信范围——这是唯一的决策因素。