鸿蒙新特性:@ohos.events.emitter 事件总线实战 —— 深入进程内发布订阅的优先级调度与生命周期管理
引言
在现代应用架构中,组件间的通信是一个永恒的话题。页面与页面之间的数据传递、模块与模块之间的状态同步、插件与宿主之间的解耦通信——这些场景都需要一个灵活、高效、低耦合的通信机制。HarmonyOS NEXT 为开发者提供了 @ohos.events.emitter 模块,这是一个进程内的事件总线(Event Bus),通过经典的发布-订阅(Pub/Sub)模式实现组件间的解耦通信。
与 Android 的 LiveData/EventBus 或 iOS 的 NotificationCenter 类似,emitter 允许应用内的任意组件订阅感兴趣的事件,并在事件发生时异步(或同步)接收通知。但与其他平台的事件总线不同,鸿蒙的 emitter 内置了四级事件优先级调度机制——IMMEDIATE(立即同步执行)、HIGH(高优先级)、LOW(低优先级)、IDLE(空闲时执行),让开发者能够精细控制事件回调的执行时序。
@ohos.events.emitter 属于 @kit.BasicServicesKit,是进程内通信的基础设施。它零权限、零配置、跨平台(API 12+ 支持 @crossplatform),可在鸿蒙手机、平板、车机等多设备形态上使用。本文将深入讲解 emitter 的核心概念、API 设计、优先级调度机制,并构建一个完整的"事件总线实验室"Demo 来直观体验这些特性。
一、核心概念与架构设计
1.1 事件总线的本质
事件总线的核心思想非常简单:发布者不需要知道订阅者是谁,订阅者也不需要知道事件是谁发出的。两者通过一个"事件 ID"(字符串或数字)进行关联:
发布者 事件总线 订阅者
| | |
|-- emit('order.created') -->| |
| |-- callback(data) ------->|
| |-- callback(data) ------->| (多个订阅者)
这种设计带来了几个重要的架构优势:
- 解耦:发布者和订阅者之间没有直接的代码依赖
- 灵活:可以在运行时动态添加或移除订阅者
- 扩展:一个事件可以被多个订阅者同时监听
- 简单:API 极其简洁,学习成本低
1.2 与跨进程通信的区别
在鸿蒙生态中,@ohos.events.emitter 和 @ohos.commonEventManager 都提供了事件通信能力,但它们的应用场景完全不同:
| 特性 | emitter | commonEventManager |
|---|---|---|
| 通信范围 | 进程内 | 跨进程/跨应用 |
| 性能 | 极低延迟 | 有一定 IPC 开销 |
| 权限 | 无需任何权限 | 需要声明权限 |
| 配置 | 零配置 | 需要在配置文件中声明 |
| 适用场景 | UI 组件通信、状态同步 | 系统广播、应用间通知 |
| 优先级调度 | 支持 4 级 | 不适用 |
简单来说:应用内部组件之间的通信用 emitter,应用之间或应用与系统之间的通信用 commonEventManager。
1.3 API 架构总览
emitter 的 API 可以划分为四个核心操作:
| 操作 | 方法 | 说明 |
|---|---|---|
| 持久订阅 | on(eventId, callback) |
订阅事件,每次事件触发都会执行回调 |
| 一次性订阅 | once(eventId, callback) |
订阅事件,回调执行一次后自动取消订阅 |
| 取消订阅 | off(eventId) / off(eventId, callback) |
取消指定事件的所有订阅或特定回调 |
| 发送事件 | emit(eventId, data?) / emit(eventId, options, data?) |
发送事件,可选优先级和数据载荷 |
| 查询监听 | getListenerCount(eventId) |
获取指定事件的订阅者数量 |
EventId:事件标识符,可以是 string 或 number 类型。从 API 11 开始,推荐使用 string 类型,更具可读性。例如 "order.created"、"user.login"、"data.updated" 等语义化的字符串 ID。
EventData:事件携带的数据。结构为 { data?: { [key: string]: any } },其中 data 字段承载任意键值对。发送时的数据会透传给所有订阅者的回调函数。
Options:发送选项。目前主要包含 priority 字段,用于指定事件的调度优先级。
二、EventPriority:四级优先级调度机制
鸿蒙 emitter 最具特色的设计是其内置的四级事件优先级系统。通过 EventPriority 枚举控制事件回调的执行时序:
2.1 四级优先级
enum EventPriority {
IMMEDIATE = 0, // 立即执行(同步)
HIGH = 1, // 高优先级(异步,优先调度)
LOW = 2, // 低优先级(异步,延后调度)
IDLE = 3 // 空闲时执行(异步,最低优先级)
}
2.2 IMMEDIATE —— 同步立即执行
IMMEDIATE 是最特殊的优先级。当使用 IMMEDIATE 优先级发送事件时,emit() 会同步触发所有订阅者的回调函数,在回调全部执行完毕后,emit() 才返回:
emitter.on('data.process', (data) => {
console.log('1. 开始处理');
// 执行耗时操作...
console.log('2. 处理完成');
});
console.log('A. 发送前');
emitter.emit('data.process', { priority: emitter.EventPriority.IMMEDIATE });
console.log('B. 发送后');
// 输出顺序:
// A. 发送前
// 1. 开始处理
// 2. 处理完成
// B. 发送后
这种同步特性让 IMMEDIATE 非常适合需要获取回调执行结果的场景。例如,一个数据验证管道,在 emit 返回后就可以确定所有验证器是否通过。
2.3 HIGH / LOW / IDLE —— 异步优先级队列
HIGH、LOW、IDLE 三种优先级都是异步执行的。当使用这些优先级发送事件时,emit() 会立即返回,回调函数被放入对应优先级的任务队列中,由事件循环调度执行:
console.log('A');
emitter.emit('test', { priority: emitter.EventPriority.HIGH });
console.log('B');
emitter.emit('test', { priority: emitter.EventPriority.LOW });
console.log('C');
emitter.emit('test', { priority: emitter.EventPriority.IDLE });
console.log('D');
// A → B → C → D(emit 全部返回)
// 然后回调按 HIGH → LOW → IDLE 顺序执行
- HIGH:高优先级回调会尽快被调度执行,适合用户交互响应、UI 更新等
- LOW:低优先级回调延后执行,适合数据持久化、日志记录等非紧急操作
- IDLE:最低优先级,在事件循环空闲时才执行,适合预加载、缓存清理等可延迟操作
2.4 优先级选择的实践建议
| 场景 | 推荐优先级 | 原因 |
|---|---|---|
| 表单验证、权限检查 | IMMEDIATE | 需要同步获取结果 |
| UI 状态更新 | HIGH | 需要尽快响应用户 |
| 数据持久化 | LOW | 可延迟,不影响 UI |
| 预加载、分析上报 | IDLE | 空闲时执行,不抢占资源 |
三、核心 API 详解
3.1 on() —— 持久订阅
on() 创建一个持久的事件订阅,每次对应事件被触发时,回调函数都会被调用:
// 使用字符串作为事件 ID(推荐,API 11+)
emitter.on('order.created', (data: emitter.EventData) => {
const payload = data.data;
console.log('新订单:', payload?.orderId);
});
// 使用数字作为事件 ID(兼容旧版本)
emitter.on(1001, (data: emitter.EventData) => {
console.log('事件 1001 触发');
});
持久订阅的生命周期:
- 订阅一直有效,直到显式调用
off()取消 - 即使事件没有触发,回调也保持在内存中
- 组件销毁时需要在
aboutToDisappear中取消订阅
3.2 once() —— 一次性订阅
once() 创建一个一次性的事件订阅,回调函数执行一次后自动取消订阅:
emitter.once('user.login', (data: emitter.EventData) => {
console.log('用户登录成功,这是一次性通知');
// 此回调执行后自动取消订阅
});
once() 非常适合以下场景:
- 一次性通知:如"数据加载完成"通知,只需要处理一次
- 请求-响应模式:发送请求后,用
once等待响应 - 初始化回调:等待某个条件满足后执行初始化,执行一次后不再需要
3.3 off() —— 取消订阅
off() 用于取消事件订阅。有两种重载形式:
// 取消指定事件的所有订阅者
emitter.off('order.created');
// 取消指定事件的特定回调(需要持有回调引用)
const callback = (data: emitter.EventData) => { /* ... */ };
emitter.on('order.created', callback);
// ... 稍后取消
emitter.off('order.created', callback);
最佳实践是在组件销毁时清理所有订阅:
aboutToDisappear(): void {
emitter.off('order.created');
emitter.off('user.login');
emitter.off('data.updated');
}
3.4 emit() —— 发送事件
emit() 是事件的触发器。它有多重重载形式:
// 基础发送(默认优先级)
emitter.emit('order.created', {
data: { orderId: 'ORD-001', amount: 99.9 }
});
// 指定优先级发送
emitter.emit('order.created',
{ priority: emitter.EventPriority.IMMEDIATE },
{ data: { orderId: 'ORD-001' } }
);
// 无数据发送(仅通知)
emitter.emit('config.changed');
3.5 getListenerCount() —— 查询订阅者数量
用于调试和监控的辅助方法:
const count = emitter.getListenerCount('order.created');
console.log(`当前有 ${count} 个订阅者监听 order.created`);
四、生命周期管理
与所有事件订阅机制一样,emitter 的订阅需要妥善管理生命周期。如果不在组件销毁时取消订阅,会导致:
- 内存泄漏:回调函数持有组件引用(通过闭包),导致组件无法被 GC 回收
- 逻辑错误:已销毁的组件仍然响应事件,可能导致状态更新到不存在的 UI 上
- 性能下降:无用的回调累积,增加事件处理开销
4.1 正确的清理模式
@Entry
@Component
struct MyPage {
aboutToAppear(): void {
// 在组件出现时订阅
emitter.on('data.updated', (data: emitter.EventData) => {
this.handleDataUpdate(data);
});
}
aboutToDisappear(): void {
// 在组件消失时取消订阅
emitter.off('data.updated');
}
}
4.2 使用 once() 简化清理
如果确定只需要处理一次事件,使用 once() 可以避免手动清理:
aboutToAppear(): void {
// 使用 once,无需手动 off
emitter.once('init.complete', (data: emitter.EventData) => {
this.initialize(data);
});
}


五、实战 Demo:事件总线实验室
下面我们构建一个完整的事件总线实验室页面,通过可视化的方式体验 emitter 的所有核心特性。
5.1 页面设计
页面核心设计包含三个预设的事件通道:
| 通道 ID | 标签 | 颜色 | 说明 |
|---|---|---|---|
order.created |
订单创建 | 绿色 #10B981 | 模拟电商订单创建事件 |
payment.completed |
支付完成 | 蓝色 #0EA5E9 | 模拟支付完成事件 |
notification.received |
通知到达 | 橙色 #F97316 | 模拟通知接收事件 |
每个通道支持两种订阅模式:
- on() 持久订阅:点击"订阅(on)"后,每次事件触发都会收到通知
- once() 一次性订阅:点击"一次性(once)"后,只会在下一次事件触发时收到通知,之后自动取消
5.2 核心实现
订阅管理——通过 toggleSubscribe 方法切换持久订阅状态:
private toggleSubscribe(index: number): void {
const ch = this.channels[index];
if (ch.subscribed) {
emitter.off(ch.id);
this.updateChannel(index, false, ch.onceSubscribed);
this.addLog(ch.id, '取消订阅', '-', '-');
} else {
emitter.on(ch.id, (data: emitter.EventData) => {
const payload = data.data !== undefined ?
JSON.stringify(data.data) : '无数据';
this.addLog(ch.id, '收到事件', '-', payload);
});
this.updateChannel(index, true, ch.onceSubscribed);
this.addLog(ch.id, '订阅成功', '-', '-');
}
}
发送事件——支持选择目标通道、优先级和附带数据:
private emitEvent(): void {
const priority = this.priorityValues[this.selectedPriority];
const dataPayload: emitter.EventData = this.emitDataText.trim() !== '' ?
{ data: { message: this.emitDataText.trim() } } :
{ data: { timestamp: Date.now() } };
emitter.emit(this.selectedChannel,
{ priority: priority },
dataPayload
);
this.addLog(this.selectedChannel, '发送事件',
this.priorityLabels[this.selectedPriority],
this.emitDataText.trim() || '(空数据)');
}
毫秒级事件日志——记录每个事件的精确时间戳:
private addLog(channel: string, direction: string,
priority: string, detail: string): void {
const now = new Date();
const ts = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0') + '.' +
now.getMilliseconds().toString().padStart(3, '0');
const entry: EmitterLog = {
time: ts, channel: channel, direction: direction,
priority: priority, detail: detail
};
this.events = [entry].concat(this.events).slice(0, 50);
}
5.3 交互方式
Demo 页面提供四个核心交互点:
-
订阅管理:每个通道都有"订阅(on)"和"一次性(once)"两个按钮,点击后通过
emitter.on()或emitter.once()注册回调,再次点击则通过emitter.off()取消订阅。getListenerCount()实时显示当前订阅者数量。 -
事件发送:在"发送事件"面板中,首先选择目标通道(订单/支付/通知),然后选择优先级(IMMEDIATE 红色/ HIGH 橙色/ LOW 蓝色/ IDLE 灰色),可选输入附带数据,点击"发送"按钮触发事件。
-
优先级对比:由于 IMMEDIATE 是同步执行的,而其他优先级是异步的,你可以通过事件日志中的毫秒级时间戳直观观察不同优先级的执行时序差异。
-
事件日志:所有事件操作(订阅、取消、发送、接收)都以表格形式记录,包含精确到毫秒的时间戳、通道名、方向(发送/收到)、优先级和数据内容。
5.4 实验步骤建议
- 点击"订单创建"的"订阅(on)“按钮 → 日志显示"订阅成功”
- 在发送面板选择"订单创建"通道,优先级选 IMMEDIATE,数据输入 “test order”
- 点击"发送" → 日志同时显示"发送事件"和"收到事件",时间戳几乎相同(IMMEDIATE 同步特性)
- 切换到 HIGH 优先级再发送 → 观察发送和收到之间的时间差(异步特性)
- 点击"一次性(once)" → 发送一次事件后,once 标记自动消失
- 订阅多个通道后,发送事件观察
getListenerCount的变化
六、与 commonEventManager 的协作模式
在一个完整的鸿蒙应用中,emitter 和 commonEventManager 经常配合使用:
import emitter from '@ohos.events.emitter';
import commonEventManager from '@ohos.commonEventManager';
// 监听系统广播(跨进程)
const subscriber = commonEventManager.createSubscriberSync({
events: ['usual.event.SCREEN_ON']
});
commonEventManager.subscribe(subscriber, (err, data) => {
// 将系统事件转换为应用内事件
emitter.emit('system.screenOn',
{ priority: emitter.EventPriority.HIGH });
});
// 应用内组件订阅应用内事件
emitter.on('system.screenOn', (data: emitter.EventData) => {
// 更新 UI,恢复播放等
this.resumePlayback();
});
这种"系统事件 → emitter 应用内事件"的桥接模式,可以将跨进程的系统广播转换为低延迟的进程内事件,既保持了架构的灵活性,又避免了各个组件都去直接订阅系统广播的复杂性。
七、ArkTS 严格模式下的类型处理
在 ArkTS 严格模式下使用 emitter 时,需要注意以下几点:
7.1 回调参数类型
虽然 emitter.EventData 的 data 字段定义为 { [key: string]: any },但这是 SDK 内置类型,ArkTS 编译器不会对其报错。在回调中使用时:
emitter.on('order.created', (data: emitter.EventData) => {
// data.data 的类型是 { [key: string]: any } | undefined
const payload = data.data;
if (payload !== undefined) {
console.log(JSON.stringify(payload));
}
});
7.2 ForEach 回调
在 ArkTS 严格模式下,ForEach 的回调参数需要显式类型注解:
// 正确:显式类型
ForEach(this.channels, (ch: ChannelState, index: number) => {
// ...
})
// 错误:隐式类型(arkts-no-untyped-obj-literals)
ForEach(this.channels, (ch, index) => {
// ...
})
7.3 状态更新中的数组操作
更新 @State 数组时,需要注意 arkts-no-noninferrable-arr-literals 规则:
// 正确:先声明类型化变量
const entry: EmitterLog = { time: ts, channel: channel,
direction: direction, priority: priority, detail: detail };
const newEvents: EmitterLog[] = [entry];
this.events = newEvents.concat(this.events).slice(0, 50);
// 错误:内联数组字面量
this.events = [{ time: ts, ... }].concat(this.events).slice(0, 50);
八、总结
@ohos.events.emitter 是 HarmonyOS NEXT 中实现进程内组件解耦通信的核心工具。通过本文的学习,你应该已经掌握:
- 核心概念:发布-订阅模式在进程内通信中的应用
- 四种优先级:IMMEDIATE(同步)、HIGH、LOW、IDLE(异步)的选择场景
- 持久订阅与一次性订阅:
on()与once()的差异和适用场景 - 生命周期管理:在组件销毁时取消订阅,避免内存泄漏
- 与 commonEventManager 的协作:跨进程与进程内事件的桥接模式
emitter 的设计体现了 HarmonyOS 在 API 设计上的几个核心理念:
- 简单易用:零权限、零配置、即开即用
- 精细控制:四级优先级提供灵活的执行时序选择
- 跨平台:API 12+ 支持跨设备形态使用
- 类型安全:泛型支持确保编译期类型检查
在实际项目中,emitter 最常见的应用场景包括:
- 全局状态管理:替代简单的状态管理库,通过事件通知状态变更
- 页面间通信:跨页面的数据传递和操作通知
- 插件化架构:宿主与插件之间的松耦合通信
- UI 组件解耦:复杂页面中多个子组件之间的协同
更多推荐




所有评论(0)