在 React Native 开发中,我们经常需要接收来自原生(Native)端的事件,比如定位更新、推送通知或传感器数据。
翻看文档或现有代码,你会发现有两种方式可以实现:DeviceEventEmitter 和 NativeEventEmitter。
既然两者都能接收事件,为什么 React Native 需要设计两个不同的 API?它们在实际应用中究竟有何区别?
本文将从直观图解、使用场景、底层原理及Native 端实现四个维度,带你彻底搞懂这两者的差异。
一、 直观图解:广播 vs 专线
如果把 App 比作一个大公司,Native 端是“管理层”,JS 端是“员工”。
1. DeviceEventEmitter:村口大喇叭(全局广播)
DeviceEventEmitter 就像是村口的大喇叭。
- 机制:Native 只要一喊(发送事件),所有在 JS 端竖着耳朵(addListener)的人都能听到。
- 特点:Native 不知道有没有人在听,反正我喊了。
- 关系:多对多,松散耦合。
graph TD
A[Native Module A] -->|Event X| Bus[Global Event Bus]
B[Native Module B] -->|Event Y| Bus
C[Native Module C] -->|Event Z| Bus
Bus -->|Broadcast| P1[JS Page 1]
Bus -->|Broadcast| P2[JS Page 2]
style Bus fill:#f96,stroke:#333,stroke-width:2px,color:white
2. NativeEventEmitter:专线电话(模块绑定)
NativeEventEmitter 就像是部门内部的专线电话。
- 机制:你必须先找到特定的 Native 模块(比如定位模块),然后建立连接。
- 特点:Native 知道有没有人在听。如果 JS 端没人接电话,Native 端就可以挂断(停止发送),节省话费(电量/性能)。
- 关系:一对一,强绑定。
sequenceDiagram
participant JS as JS Service
participant Native as Native Module
JS->>Native: addListener (Handshake)
Note right of Native: 🟢 Start Sensor/Task
loop Data Stream
Native-->>JS: Event Data
end
JS->>Native: removeListener
Note right of Native: 🔴 Stop Sensor/Task
二、 核心区别:资源管理的智慧(计步器案例)
表面上看,两者只是代码写法不同,但核心差异在于 对 Native 端资源的控制能力。
场景假设:我们需要开发一个“计步器”功能,需要调用手机的传感器。
1. 如果使用 DeviceEventEmitter (大喇叭)
由于是全局广播,Native 端无法感知 JS 端是否订阅了事件。
- 后果:为了确保 JS 能收到步数,Native 模块可能在 App 启动时就开启传感器,并一直发送事件。
- 问题:即使通过 JS 只有“运动详情页”需要展示步数,当用户在“个人中心”或“首页”时,传感器依然在后台疯狂运转,导致手机发烫、耗电快。
2. 如果使用 NativeEventEmitter (专线)
这是官方推荐的方式,因为它打通了 JS 与 Native 的状态同步。
- 机制:
- 当 JS 端调用
addListener时,Native 端会收到通知:“有人在听了,干活吧”。 - 当 JS 端移除最后一个监听器时,Native 端也会收到通知:“没人听了,休息吧”。
- 当 JS 端调用
- 优势:按需开启硬件资源。即“有人听才说,没人听就停”。
三、 Native 端如何配合?(逻辑闭环)
这是初学者最容易忽略的部分。要实现上述的“按需省电”,不仅 JS 要写对,Native 端(iOS/Android)也必须实现对应的接口。
1. iOS 端实现 (Objective-C/Swift)
iOS 封装得非常完美,你只需要继承 RCTEventEmitter 并重写两个方法:
// MyPedometer.m
#import "React/RCTEventEmitter.h"
@interface MyPedometer : RCTEventEmitter <RCTBridgeModule>
@end
@implementation MyPedometer
// 1. 必须重写:返回支持的事件名列表
- (NSArray<NSString *> *)supportedEvents {
return @[@"onStepChange"];
}
// 2. 核心:当 JS 端有了第一个监听者时调用
- (void)startObserving {
// 【在这里开启传感器】
[self.sensorManager start];
}
// 3. 核心:当 JS 端移除了最后一个监听者时调用
- (void)stopObserving {
// 【在这里关闭传感器,节省电量】
[self.sensorManager stop];
}
@end
2. Android 端实现 (Java/Kotlin)
Android 原生层稍微麻烦一点,需要手动配合 JS 端的 NativeEventEmitter 调用。你需要显式定义 addListener 和 removeListeners 方法。
// MyPedometerModule.java
public class MyPedometerModule extends ReactContextBaseJavaModule {
private int listenerCount = 0;
// ... 构造函数等 ...
// 必须定义这个方法,JS 端的 NativeEventEmitter 会自动调用它
@ReactMethod
public void addListener(String eventName) {
if (listenerCount == 0) {
// 【开启传感器】
startSensor();
}
listenerCount++;
}
// 必须定义这个方法,JS 端的 NativeEventEmitter 会自动调用它
@ReactMethod
public void removeListeners(Integer count) {
listenerCount -= count;
if (listenerCount == 0) {
// 【关闭传感器】
stopSensor();
}
}
}
注意:如果你在 Android 端不写这两个
@ReactMethod,JS 端使用new NativeEventEmitter(Module)时可能会报错或警告,且无法实现自动管理资源的效果。
四、 JS 端代码对比
1. DeviceEventEmitter (不推荐,除非维护旧代码)
import { DeviceEventEmitter } from 'react-native';
// 就像注册一个全局事件,名字对上就能收到
const subscription = DeviceEventEmitter.addListener('onGlobalEvent', (event) => {
console.log('收到:', event);
});
// 记得移除
// subscription.remove();
2. NativeEventEmitter (推荐)
import { NativeEventEmitter, NativeModules } from 'react-native';
// 1. 先拿到具体的 Native 模块
const { MyPedometer } = NativeModules;
// 2. 创建该模块专属的 Emitter
// 这一步很重要,它建立了 JS 和 Native 的连接对象
const pedometerEmitter = new NativeEventEmitter(MyPedometer);
// 3. 订阅事件
// 底层会自动调用 Native 端的 addListener/startObserving
const subscription = pedometerEmitter.addListener('onStepChange', (steps) => {
console.log('当前步数:', steps);
});
// 4. 移除订阅
// 当最后一个监听移除时,底层自动调用 Native 端的 removeListeners/stopObserving
// subscription.remove();
五、 源码浅析(进阶理解)
为什么 NativeEventEmitter 能做到这一点?我们看一眼源码的核心逻辑(已简化):
// NativeEventEmitter.js 伪代码
class NativeEventEmitter extends EventEmitter {
constructor(nativeModule) {
super();
// 强绑定传入的 Native 模块
this._nativeModule = nativeModule;
}
addListener(eventType, listener, context) {
// 1. 先告诉 Native 模块:我要监听了!
if (this._nativeModule) {
// 这就是为什么 Android 端需要写 addListener 方法
// iOS 端则是通过 Bridge 机制自动触发 startObserving
this._nativeModule.addListener(eventType);
}
// 2. 然后再在 JS 层注册回调
return super.addListener(eventType, listener, context);
}
removeSubscription(subscription) {
if (this._nativeModule) {
// 告诉 Native 模块:我不听了!
this._nativeModule.removeListeners(1);
}
super.removeSubscription(subscription);
}
}
可以看到,NativeEventEmitter 本质上就是给普通的 Event Emitter 加了一层钩子(Hook),在添加和移除监听时,顺便通知了一下 Native 模块。
六、 总结与最佳实践
| 特性 | DeviceEventEmitter | NativeEventEmitter |
|---|---|---|
| 作用域 | 全局广播 | 模块私有 |
| 资源管理 | 无(Native 无法感知) | 有(支持按需开启/关闭) |
| 代码规范 | 容易命名冲突 | 结构清晰,模块化 |
| 推荐指数 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
最佳实践建议:
- 默认使用
NativeEventEmitter:无论是新建模块还是重构,只要涉及 Native 通信,请首选此方式。 - Native 端务必配合:在 Native 代码中实现
startObserving(iOS) 或addListener(Android),利用这个机制来管理耗电的后台任务(定位、蓝牙、传感器等)。 - 及时移除监听:虽然机制很完善,但 JS 端组件卸载(Unmount)时,依然要记得调用
subscription.remove(),防止内存泄漏。