Skip to content

Commit

Permalink
Fixed NativeEventListener deregistration
Browse files Browse the repository at this point in the history
Summary:
The `EmitterSubscription.remove()` method was previously calling `this.subscriber.removeSubscription(this)` directly, bypassing the mechanism in `NativeEventEmitter` that keeps track of the number of subscriptions.

This meant that native event modules (subclasses of `RCTEventEmitter`) would keep sending events even after all the listeners had been removed. This wasn't a huge overhead, since these modules are singletons and only send one message over the bridge per event, regardless of the number of listeners, but it's still undesirable.

This fixes the problem by routing the `EmitterSubscription.remove()` method through the `EventEmitter` so that `NativeEventEmitter` can apply the additional native calls.

I've also improved the architecture so that each `NativeEventEmitter` uses its own `EventEmitter`, but they currently all still share the same `EventSubscriptionVendor` so that legacy code which registers events via `RCTDeviceEventEmitter` still works.

Reviewed By: vjeux

Differential Revision: D3292361

fbshipit-source-id: d60e881d50351523d2112473703bea826641cdef
  • Loading branch information
nicklockwood authored and Facebook Github Bot 1 committed May 16, 2016
1 parent 9a899be commit 516bf7b
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 254 deletions.
4 changes: 2 additions & 2 deletions Libraries/BugReporting/BugReporting.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
const BugReportingNativeModule = require('NativeModules').BugReporting;
const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');

import type {EmitterSubscription} from 'EventEmitter';
import type EmitterSubscription from 'EmitterSubscription';

type ExtraData = { [key: string]: string };
type SourceCallback = () => string;
Expand All @@ -28,7 +28,7 @@ type SourceCallback = () => string;
class BugReporting {

static _sources: Map<string, SourceCallback> = new Map();
static _subscription: EmitterSubscription = null;
static _subscription: ?EmitterSubscription = null;

/**
* `init` is called in `AppRegistry.runApplication`, so you shouldn't have to worry about it.
Expand Down
62 changes: 62 additions & 0 deletions Libraries/EventEmitter/EmitterSubscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule EmitterSubscription
* @noflow
* @typechecks
*/
'use strict';

const EventSubscription = require('EventSubscription');

import type EventEmitter from 'EventEmitter';
import type EventSubscriptionVendor from 'EventSubscriptionVendor';

/**
* EmitterSubscription represents a subscription with listener and context data.
*/
class EmitterSubscription extends EventSubscription {

emitter: EventEmitter;
listener: Function;
context: ?Object;

/**
* @param {EventEmitter} emitter - The event emitter that registered this
* subscription
* @param {EventSubscriptionVendor} subscriber - The subscriber that controls
* this subscription
* @param {function} listener - Function to invoke when the specified event is
* emitted
* @param {*} context - Optional context object to use when invoking the
* listener
*/
constructor(
emitter: EventEmitter,
subscriber: EventSubscriptionVendor,
listener: Function,
context: ?Object
) {
super(subscriber);
this.emitter = emitter;
this.listener = listener;
this.context = context;
}

/**
* Removes this subscription from the emitter that registered it.
* Note: we're overriding the `remove()` method of EventSubscription here
* but deliberately not calling `super.remove()` as the responsibility
* for removing the subscription lies with the EventEmitter.
*/
remove() {
this.emitter.removeSubscription(this);
}
}

module.exports = EmitterSubscription;
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
*
* @providesModule EventEmitter
* @noflow
* @typechecks
* @typecheck
*/

var EmitterSubscription = require('EmitterSubscription');
var ErrorUtils = require('ErrorUtils');
var EventSubscriptionVendor = require('EventSubscriptionVendor');
var emptyFunction = require('fbjs/lib/emptyFunction');
var invariant = require('fbjs/lib/invariant');
const EmitterSubscription = require('EmitterSubscription');
const EventSubscriptionVendor = require('EventSubscriptionVendor');
const emptyFunction = require('fbjs/lib/emptyFunction');
const invariant = require('fbjs/lib/invariant');

/**
* @class EventEmitter
Expand All @@ -31,11 +30,18 @@ var invariant = require('fbjs/lib/invariant');
* more advanced emitter may use an EventHolder and EventFactory.
*/
class EventEmitter {

_subscriber: EventSubscriptionVendor;
_currentSubscription: ?EmitterSubscription;

/**
* @constructor
*
* @param {EventSubscriptionVendor} subscriber - Optional subscriber instance
* to use. If omitted, a new subscriber will be created for the emitter.
*/
constructor() {
this._subscriber = new EventSubscriptionVendor();
constructor(subscriber: ?EventSubscriptionVendor) {
this._subscriber = subscriber || new EventSubscriptionVendor();
}

/**
Expand All @@ -53,10 +59,12 @@ class EventEmitter {
* listener
*/
addListener(
eventType: String, listener, context: ?Object): EmitterSubscription {
return this._subscriber.addSubscription(
eventType: string, listener: Function, context: ?Object): EmitterSubscription {

return (this._subscriber.addSubscription(
eventType,
new EmitterSubscription(this._subscriber, listener, context));
new EmitterSubscription(this, this._subscriber, listener, context)
) : any);
}

/**
Expand All @@ -69,10 +77,9 @@ class EventEmitter {
* @param {*} context - Optional context object to use when invoking the
* listener
*/
once(eventType: String, listener, context: ?Object): EmitterSubscription {
var emitter = this;
return this.addListener(eventType, function() {
emitter.removeCurrentListener();
once(eventType: string, listener: Function, context: ?Object): EmitterSubscription {
return this.addListener(eventType, () => {
this.removeCurrentListener();
listener.apply(context, arguments);
});
}
Expand All @@ -84,7 +91,7 @@ class EventEmitter {
* @param {?string} eventType - Optional name of the event whose registered
* listeners to remove
*/
removeAllListeners(eventType: ?String) {
removeAllListeners(eventType: ?string) {
this._subscriber.removeAllSubscriptions(eventType);
}

Expand Down Expand Up @@ -114,7 +121,19 @@ class EventEmitter {
!!this._currentSubscription,
'Not in an emitting cycle; there is no current subscription'
);
this._subscriber.removeSubscription(this._currentSubscription);
this.removeSubscription(this._currentSubscription);
}

/**
* Removes a specific subscription. Called by the `remove()` method of the
* subscription itself to ensure any necessary cleanup is performed.
*/
removeSubscription(subscription: EmitterSubscription) {
invariant(
subscription.emitter === this,
'Subscription does not belong to this emitter.'
);
this._subscriber.removeSubscription(subscription);
}

/**
Expand All @@ -124,8 +143,8 @@ class EventEmitter {
* @param {string} eventType - Name of the event to query
* @returns {array}
*/
listeners(eventType: String): Array /* TODO: Array<EventSubscription> */ {
var subscriptions = this._subscriber.getSubscriptionsForType(eventType);
listeners(eventType: string): [EmitterSubscription] {
const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any);
return subscriptions
? subscriptions.filter(emptyFunction.thatReturnsTrue).map(
function(subscription) {
Expand All @@ -148,13 +167,11 @@ class EventEmitter {
*
* emitter.emit('someEvent', 'abc'); // logs 'abc'
*/
emit(eventType: String) {
var subscriptions = this._subscriber.getSubscriptionsForType(eventType);
emit(eventType: string) {
const subscriptions: ?[EmitterSubscription] = (this._subscriber.getSubscriptionsForType(eventType): any);
if (subscriptions) {
var keys = Object.keys(subscriptions);
for (var ii = 0; ii < keys.length; ii++) {
var key = keys[ii];
var subscription = subscriptions[key];
for (let i = 0, l = subscriptions.length; i < l; i++) {
const subscription = subscriptions[i];

// The subscription may have been removed during this event loop.
if (subscription) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
/**
* @generated SignedSource<<fb2bb5c1c402a097a7e1da7413526629>>
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* !! This file is a check-in of a static_upstream project! !!
* !! !!
* !! You should not modify this file directly. Instead: !!
* !! 1) Use `fjs use-upstream` to temporarily replace this with !!
* !! the latest version from upstream. !!
* !! 2) Make your changes, test them, etc. !!
* !! 3) Use `fjs push-upstream` to copy your changes back to !!
* !! static_upstream. !!
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule EventEmitterWithHolding
* @typechecks
* @flow
*/
'use strict';

import type EmitterSubscription from 'EmitterSubscription';
import type EventEmitter from 'EventEmitter';
import type EventHolder from 'EventHolder';

/**
* @class EventEmitterWithHolding
* @description
Expand All @@ -30,14 +28,20 @@
* that uses an emitter.
*/
class EventEmitterWithHolding {

_emitter: EventEmitter;
_eventHolder: EventHolder;
_currentEventToken: ?Object;
_emittingHeldEvents: boolean;

/**
* @constructor
* @param {object} emitter - The object responsible for emitting the actual
* events.
* @param {object} holder - The event holder that is responsible for holding
* and then emitting held events.
*/
constructor(emitter, holder) {
constructor(emitter: EventEmitter, holder: EventHolder) {
this._emitter = emitter;
this._eventHolder = holder;
this._currentEventToken = null;
Expand All @@ -47,14 +51,14 @@ class EventEmitterWithHolding {
/**
* @see EventEmitter#addListener
*/
addListener(eventType: String, listener, context: ?Object) {
addListener(eventType: string, listener: Function, context: ?Object) {
return this._emitter.addListener(eventType, listener, context);
}

/**
* @see EventEmitter#once
*/
once(eventType: String, listener, context: ?Object) {
once(eventType: string, listener: Function, context: ?Object) {
return this._emitter.once(eventType, listener, context);
}

Expand All @@ -79,8 +83,8 @@ class EventEmitterWithHolding {
* }); // logs 'abc'
*/
addRetroactiveListener(
eventType: String, listener, context: ?Object): EmitterSubscription {
var subscription = this._emitter.addListener(eventType, listener, context);
eventType: string, listener: Function, context: ?Object): EmitterSubscription {
const subscription = this._emitter.addListener(eventType, listener, context);

this._emittingHeldEvents = true;
this._eventHolder.emitToListener(eventType, listener, context);
Expand All @@ -92,7 +96,7 @@ class EventEmitterWithHolding {
/**
* @see EventEmitter#removeAllListeners
*/
removeAllListeners(eventType: String) {
removeAllListeners(eventType: string) {
this._emitter.removeAllListeners(eventType);
}

Expand All @@ -106,15 +110,15 @@ class EventEmitterWithHolding {
/**
* @see EventEmitter#listeners
*/
listeners(eventType: String) /* TODO: Annotate return type here */ {
listeners(eventType: string) /* TODO: Annotate return type here */ {
return this._emitter.listeners(eventType);
}

/**
* @see EventEmitter#emit
*/
emit(eventType: String, a, b, c, d, e, _) {
this._emitter.emit(eventType, a, b, c, d, e, _);
emit(eventType: string, ...args: any) {
this._emitter.emit(eventType, ...args);
}

/**
Expand All @@ -132,20 +136,17 @@ class EventEmitterWithHolding {
* console.log(message);
* }); // logs 'abc'
*/
emitAndHold(eventType: String, a, b, c, d, e, _) {
this._currentEventToken = this._eventHolder.holdEvent(
eventType,
a, b, c, d, e, _
);
this._emitter.emit(eventType, a, b, c, d, e, _);
emitAndHold(eventType: string, ...args: any) {
this._currentEventToken = this._eventHolder.holdEvent(eventType, ...args);
this._emitter.emit(eventType, ...args);
this._currentEventToken = null;
}

/**
* @see EventHolder#releaseCurrentEvent
*/
releaseCurrentEvent() {
if (this._currentEventToken !== null) {
if (this._currentEventToken) {
this._eventHolder.releaseEvent(this._currentEventToken);
} else if (this._emittingHeldEvents) {
this._eventHolder.releaseCurrentEvent();
Expand All @@ -156,7 +157,7 @@ class EventEmitterWithHolding {
* @see EventHolder#releaseEventType
* @param {string} eventType
*/
releaseHeldEventType(eventType: String) {
releaseHeldEventType(eventType: string) {
this._eventHolder.releaseEventType(eventType);
}
}
Expand Down
Loading

0 comments on commit 516bf7b

Please sign in to comment.