鸿蒙新特性实战:@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()触发后自动注销 - 事件优先级调度:
EventPriority(IMMEDIATE / HIGH / LOW / IDLE)的执行顺序 - 带数据事件:
EventData的data字段传递任意键值对 - 监听者计数:
getListenerCount()运行时查询活跃监听数 - 隔离通道:
new Emitter()创建独立事件通道(API 22 新特性) - 全局 vs 实例对比:何时用全局 emitter,何时用 Emitter 类
环境与权限
@ohos.events.emitter属于BasicServicesKit,自 API 7 开始提供(Emitter 类自 API 22 起)。导入方式:
importemitterfrom'@ohos.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是早期 API(API 7)的事件描述方式,使用数字 ID。现代代码推荐使用字符串eventId的形式,可读性更好。priority仅在emit()时生效——订阅时的priority参数被忽略。
1.3 EventPriority — 优先级枚举
enumEventPriority{IMMEDIATE=0,// 立即执行(最高优先级)HIGH,// 高优先级LOW,// 低优先级(默认)IDLE// 空闲时执行(最低优先级)}事件优先级决定了同一eventId的多个监听器的执行顺序。注意:优先级是对发送方而言的——高优先级的事件先投递给所有匹配的监听器。这与commonEventManager的有序事件(按订阅者优先级串行传播)是不同的模型。
1.4 Options — 发送选项
interfaceOptions{priority?:EventPriority;// 事件发送优先级}Options配合emit(eventId, options, data)使用,用于指定本次发送的优先级。如果不传Options,默认使用EventPriority.LOW。
1.5 GenericEventData<T> — 泛型数据
interfaceGenericEventData<T>{data?:T;}API 12 引入的泛型版本,允许携带强类型数据。对于 TypeScript 项目,这提供了编译期的类型检查:
interfaceOrderEvent{orderId:string;amount:number;}emitter.on<OrderEvent>('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:清除该事件的所有监听器 - 传
eventId+callback:仅清除指定的那个监听器(要求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)— 指定优先级emit<T>(eventId, options, data)— 泛型版本(API 12+)
优先级影响多个监听器的执行次序:IMMEDIATE 先于 HIGH 先于 LOW 先于 IDLE。
2.5 查询监听数 — getListenerCount()
letcount=emitter.getListenerCount('counter_changed');console.log('当前活跃监听器: '+count);getListenerCount()返回指定eventId的当前活跃监听器数量(包括on()和once()注册的)。这在运行时诊断中非常有用——可以验证监听器是否成功注册,或者检测是否存在因off()未调用导致的累积。
三、Emitter 类:隔离事件通道(API 22)
API 22 引入的Emitter类是emitter模块最具辨识度的新特性。每个Emitter实例维护自己独立的事件-监听器映射表,不同实例之间的同名事件完全隔离。
3.1 创建实例
letchannelA=newemitter.Emitter();letchannelB=newemitter.Emitter();3.2 通道隔离演示
// 通道 A 订阅 'data_sync'channelA.on('data_sync',(data:emitter.EventData)=>{console.log('[A] 收到: '+JSON.stringify(data.data));});// 通道 B 也订阅 'data_sync'channelB.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:Callback<EventData>):void;once(eventId:string,callback:Callback<EventData>):void;off(eventId:string):void;off(eventId:string,callback:Callback<EventData>):void;emit(eventId:string,data?:EventData):void;emit(eventId:string,options:Options,data?:EventData):void;getListenerCount(eventId:string):number;}差异在于:
Emitter实例只支持字符串eventId(不支持InnerEvent数字 ID)Emitter实例没有泛型on<T>/once<T>的单独声明(但通过方法重载支持)Emitter实例不提供全局 emitter 的emit(eventId, options, data)旧版 API(InnerEvent 模式)
四、优先级调度机制
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 事件通信体系的两个支柱:
| 维度 | emitter | commonEventManager |
|---|---|---|
| 通信范围 | 进程内 | 跨进程(系统级) |
| 底层机制 | 内存事件表 | 系统 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 EventEmitter | HarmonyOS emitter |
|---|---|---|
| 事件 ID | string | Symbol | string | number |
| 优先级 | 不支持 | EventPriority(4 级) |
| 一次性订阅 | once() — 行为一致 | once() — 行为一致 |
| 监听者计数 | listenerCount() | getListenerCount() |
| 隔离实例 | new EventEmitter() | new emitter.Emitter() |
| 最大监听数 | setMaxListeners() | 无限制 |
| 泛型支持 | 社区实现 | 原生支持(API 12+) |
Node.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.channel=null;}}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 实例适合:页面级事件(表单值变更、列表项交互)、功能模块内部通信
一个典型的页面架构:
@Entry@Componentstruct OrderPage{privatechannel:emitter.Emitter=newemitter.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 22) |
Emitter.on/once/off/emit | 同全局 | 实例方法(API 22) |
EventPriority | enum | IMMEDIATE/HIGH/LOW/IDLE |
十、总结
@ohos.events.emitter是 HarmonyOS NEXT 的进程内事件通信基础设施,核心要点:
- 零权限零配置:完全运行在进程内存中,无任何额外开销
- 双重订阅模型:
on()持久 +once()一次性,精准匹配不同场景需求 - 四级优先级调度:发送侧的 IMMEDIATE → HIGH → LOW → IDLE 排序决定了监听器执行顺序
- 监听者可见性:
getListenerCount()提供了运行时诊断能力 - 隔离通道架构:API 22 的
Emitter类从根本上解决了全局事件空间的命名冲突问题
结合@ohos.commonEventManager(跨进程系统事件)和@ohos.events.emitter(进程内事件),HarmonyOS 开发者已经拥有了从进程内到跨进程的完整事件通信工具箱。选择哪个模块取决于通信范围——这是唯一的决策因素。