引言

在第 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() 持续接收 vs once() 触发后自动注销
  • 事件优先级调度EventPriority(IMMEDIATE / HIGH / LOW / IDLE)的执行顺序
  • 带数据事件EventDatadata 字段传递任意键值对
  • 监听者计数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() 有三个重载:

  1. emit(eventId, data) — 默认优先级
  2. emit(eventId, options, data) — 指定优先级
  3. 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 个交互点

  1. 全局 Emitter 订阅与发送 — 输入事件 ID 和数据,选择 ON 持久订阅或 ONCE 一次性订阅,选择优先级发送事件,在日志中观察接收确认
  2. 优先级竞赛 — 点击"运行优先级竞赛",4 个 ONCE 监听器分别以 IMMEDIATE/HIGH/LOW/IDLE 优先级发送,日志中输出实际执行顺序
  3. 隔离通道(Emitter 实例) — 创建两个 Emitter 实例,分别订阅同名事件,通过各自实例发送,验证事件完全隔离
  4. 跨通道隔离验证 — 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 的进程内事件通信基础设施,核心要点:

  1. 零权限零配置:完全运行在进程内存中,无任何额外开销
  2. 双重订阅模型on() 持久 + once() 一次性,精准匹配不同场景需求
  3. 四级优先级调度:发送侧的 IMMEDIATE → HIGH → LOW → IDLE 排序决定了监听器执行顺序
  4. 监听者可见性getListenerCount() 提供了运行时诊断能力
  5. 隔离通道架构:API 22 的 Emitter 类从根本上解决了全局事件空间的命名冲突问题

结合 @ohos.commonEventManager(跨进程系统事件)和 @ohos.events.emitter(进程内事件),HarmonyOS 开发者已经拥有了从进程内到跨进程的完整事件通信工具箱。选择哪个模块取决于通信范围——这是唯一的决策因素。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐