diff --git a/Libraries/EventEmitter/NativeEventEmitter.js b/Libraries/EventEmitter/NativeEventEmitter.js index a5ba4f108115f6..381d8cfa170a99 100644 --- a/Libraries/EventEmitter/NativeEventEmitter.js +++ b/Libraries/EventEmitter/NativeEventEmitter.js @@ -4,67 +4,96 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format - * @flow */ 'use strict'; +import { + type EventSubscription, + type IEventEmitter, +} from '../vendor/emitter/EventEmitter'; import Platform from '../Utilities/Platform'; -import EventEmitter from '../vendor/emitter/EventEmitter'; -import type EmitterSubscription from '../vendor/emitter/_EmitterSubscription'; import RCTDeviceEventEmitter from './RCTDeviceEventEmitter'; import invariant from 'invariant'; -type NativeModule = { - +addListener: (eventType: string) => void, - +removeListeners: (count: number) => void, +type NativeModule = $ReadOnly<{ + addListener: (eventType: string) => void, + removeListeners: (count: number) => void, ... -}; +}>; + +export type {EventSubscription}; /** - * Abstract base class for implementing event-emitting modules. This implements - * a subset of the standard EventEmitter node module API. + * `NativeEventEmitter` is intended for use by Native Modules to emit events to + * JavaScript listeners. If a `NativeModule` is supplied to the constructor, it + * will be notified (via `addListener` and `removeListeners`) when the listener + * count changes to manage "native memory". + * + * Currently, all native events are fired via a global `RCTDeviceEventEmitter`. + * This means event names must be globally unique, and it means that call sites + * can theoretically listen to `RCTDeviceEventEmitter` (although discouraged). */ -export default class NativeEventEmitter< - EventDefinitions: {...}, -> extends EventEmitter { +export default class NativeEventEmitter + implements IEventEmitter { _nativeModule: ?NativeModule; constructor(nativeModule: ?NativeModule) { - super(RCTDeviceEventEmitter.sharedSubscriber); if (Platform.OS === 'ios') { - invariant(nativeModule, 'Native module cannot be null.'); + invariant( + nativeModule != null, + '`new NativeEventEmitter()` requires a non-null argument.', + ); this._nativeModule = nativeModule; } } - addListener>( - eventType: K, - listener: (...$ElementType) => mixed, - context: $FlowFixMe, - ): EmitterSubscription { - if (this._nativeModule != null) { - this._nativeModule.addListener(eventType); - } - return super.addListener(eventType, listener, context); + addListener>( + eventType: TEvent, + listener: (...args: $ElementType) => mixed, + context?: mixed, + ): EventSubscription { + this._nativeModule?.addListener(eventType); + let subscription: ?EventSubscription = RCTDeviceEventEmitter.addListener( + eventType, + listener, + context, + ); + + return { + remove: () => { + if (subscription != null) { + this._nativeModule?.removeListeners(1); + subscription.remove(); + subscription = null; + } + }, + }; } - removeAllListeners>(eventType: ?K): void { - invariant(eventType, 'eventType argument is required.'); - const count = this.listenerCount(eventType); - if (this._nativeModule != null) { - this._nativeModule.removeListeners(count); - } - super.removeAllListeners(eventType); + emit>( + eventType: TEvent, + ...args: $ElementType + ): void { + // Generally, `RCTDeviceEventEmitter` is directly invoked. But this is + // included for completeness. + RCTDeviceEventEmitter.emit(eventType, ...args); } - removeSubscription>( - subscription: EmitterSubscription, + removeAllListeners>( + eventType?: ?TEvent, ): void { - if (this._nativeModule != null) { - this._nativeModule.removeListeners(1); - } - super.removeSubscription(subscription); + invariant( + eventType != null, + '`NativeEventEmitter.removeAllListener()` requires a non-null argument.', + ); + this._nativeModule?.removeListeners(this.listenerCount(eventType)); + RCTDeviceEventEmitter.removeAllListeners(eventType); + } + + listenerCount>(eventType: TEvent): number { + return RCTDeviceEventEmitter.listenerCount(eventType); } } diff --git a/Libraries/Utilities/DevSettings.js b/Libraries/Utilities/DevSettings.js index 8cadc8ef141c7a..b0c59d4e425f84 100644 --- a/Libraries/Utilities/DevSettings.js +++ b/Libraries/Utilities/DevSettings.js @@ -29,7 +29,7 @@ if (__DEV__) { const emitter = new NativeEventEmitter( NativeDevSettings, ); - const menuItems = new Map(); + const subscriptions = new Map(); DevSettings = { addMenuItem(title: string, handler: () => mixed): void { @@ -37,19 +37,19 @@ if (__DEV__) { // happen when hot reloading the module that registers the // menu items. The title is used as the id which means we // don't support multiple items with the same name. - const oldHandler = menuItems.get(title); - if (oldHandler != null) { - emitter.removeListener('didPressMenuItem', oldHandler); + let subscription = subscriptions.get(title); + if (subscription != null) { + subscription.remove(); } else { NativeDevSettings.addMenuItem(title); } - menuItems.set(title, handler); - emitter.addListener('didPressMenuItem', event => { + subscription = emitter.addListener('didPressMenuItem', event => { if (event.title === title) { handler(); } }); + subscriptions.set(title, subscription); }, reload(reason?: string): void { if (NativeDevSettings.reloadWithReason != null) { diff --git a/Libraries/vendor/emitter/EventEmitter.js b/Libraries/vendor/emitter/EventEmitter.js index 4084ce9e8e0fce..18e91bbba19258 100644 --- a/Libraries/vendor/emitter/EventEmitter.js +++ b/Libraries/vendor/emitter/EventEmitter.js @@ -17,3 +17,37 @@ import type {EventSubscription} from './EventSubscription'; export default EventEmitter; export type {EventSubscription}; + +/** + * Essential interface for an EventEmitter. + */ +export interface IEventEmitter { + /** + * Registers a listener that is called when the supplied event is emitted. + * Returns a subscription that has a `remove` method to undo registration. + */ + addListener>( + eventType: TEvent, + listener: (...args: $ElementType) => void, + context?: mixed, + ): EventSubscription; + + /** + * Emits the supplied event. Additional arguments supplied to `emit` will be + * passed through to each of the registered listeners. + */ + emit>( + eventType: TEvent, + ...args: $ElementType + ): void; + + /** + * Removes all registered listeners. + */ + removeAllListeners>(eventType?: ?TEvent): void; + + /** + * Returns the number of registered listeners for the supplied event. + */ + listenerCount>(eventType: TEvent): number; +}