React Native 事件机制深度解析:DeviceEventEmitter 与 NativeEventEmitter

14 阅读5分钟

在 React Native 开发中,我们经常需要接收来自原生(Native)端的事件,比如定位更新、推送通知或传感器数据。

翻看文档或现有代码,你会发现有两种方式可以实现:DeviceEventEmitterNativeEventEmitter

既然两者都能接收事件,为什么 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 端也会收到通知:“没人听了,休息吧”。
  • 优势按需开启硬件资源。即“有人听才说,没人听就停”。

三、 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 调用。你需要显式定义 addListenerremoveListeners 方法。

// 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 模块。


六、 总结与最佳实践

特性DeviceEventEmitterNativeEventEmitter
作用域全局广播模块私有
资源管理无(Native 无法感知)有(支持按需开启/关闭)
代码规范容易命名冲突结构清晰,模块化
推荐指数⭐⭐⭐⭐⭐⭐⭐

最佳实践建议:

  1. 默认使用 NativeEventEmitter:无论是新建模块还是重构,只要涉及 Native 通信,请首选此方式。
  2. Native 端务必配合:在 Native 代码中实现 startObserving (iOS) 或 addListener (Android),利用这个机制来管理耗电的后台任务(定位、蓝牙、传感器等)。
  3. 及时移除监听:虽然机制很完善,但 JS 端组件卸载(Unmount)时,依然要记得调用 subscription.remove(),防止内存泄漏。