鸿蒙新特性实战:@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 起)。导入方式:
import emitter from '@ohos.events.emitter';
零权限需求:整个 emitter 模块完全运行在进程内,不涉及任何系统服务调用,因此不需要任何权限声明。这是它相比 commonEventManager 的另一个优势——开箱即用,零配置。
一、核心数据类型
1.1 EventData — 事件载荷
interface EventData {
data?: { [key: string]: any };
}
EventData 是事件携带数据的容器。data 是一个可选的键值对对象,可以传递任意 JSON 可序列化数据。emitter 不做序列化——数据在进程内存中直接传递,因此可以携带比 commonEventManager(限制 64KB 字符串)更丰富的数据结构。
1.2 InnerEvent — 内事件对象
interface InnerEvent {
eventId: number; // 事件 ID(数字类型)
priority?: EventPriority; // 优先级(发送时有效,订阅时忽略)
}
InnerEvent 是早期 API(API 7)的事件描述方式,使用数字 ID。现代代码推荐使用字符串 eventId 的形式,可读性更好。priority 仅在 emit() 时生效——订阅时的 priority 参数被忽略。
1.3 EventPriority — 优先级枚举
enum EventPriority {
IMMEDIATE = 0, // 立即执行(最高优先级)
HIGH, // 高优先级
LOW, // 低优先级(默认)
IDLE // 空闲时执行(最低优先级)
}
事件优先级决定了同一 eventId 的多个监听器的执行顺序。注意:优先级是对发送方而言的——高优先级的事件先投递给所有匹配的监听器。这与 commonEventManager 的有序事件(按订阅者优先级串行传播)是不同的模型。
1.4 Options — 发送选项
interface Options {
priority?: EventPriority; // 事件发送优先级
}
Options 配合 emit(eventId, options, data) 使用,用于指定本次发送的优先级。如果不传 Options,默认使用 EventPriority.LOW。
1.5 GenericEventData<T> — 泛型数据
interface GenericEventData<T> {
data?: T;
}
API 12 引入的泛型版本,允许携带强类型数据。对于 TypeScript 项目,这提供了编译期的类型检查:
interface OrderEvent {
orderId: string;
amount: number;
}
emitter.on<OrderEvent>('order_paid', (data) => {
// data.data 类型为 OrderEvent | undefined
console.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()
let count = emitter.getListenerCount('counter_changed');
console.log('当前活跃监听器: ' + count);
getListenerCount() 返回指定 eventId 的当前活跃监听器数量(包括 on() 和 once() 注册的)。这在运行时诊断中非常有用——可以验证监听器是否成功注册,或者检测是否存在因 off() 未调用导致的累积。

三、Emitter 类:隔离事件通道(API 22)
API 22 引入的 Emitter 类是 emitter 模块最具辨识度的新特性。每个 Emitter 实例维护自己独立的事件-监听器映射表,不同实例之间的同名事件完全隔离。
3.1 创建实例
let channelA = new emitter.Emitter();
let channelB = new emitter.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 高度一致:
class Emitter {
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 优先级竞赛实验
let execOrder: 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 个关键 API
4 个交互点
- 全局 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
@Component
struct OrderPage {
private channel: emitter.Emitter = new emitter.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 开发者已经拥有了从进程内到跨进程的完整事件通信工具箱。选择哪个模块取决于通信范围——这是唯一的决策因素。
更多推荐



所有评论(0)