Skip to content
This repository was archived by the owner on Jun 3, 2024. It is now read-only.

Commit c617a6a

Browse files
Merge pull request #85 from lelandrichardson/lmr--native-event-emitter
Add NativeEventEmitter API
2 parents c5e82f8 + d0ad1d9 commit c617a6a

File tree

6 files changed

+399
-0
lines changed

6 files changed

+399
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
const EventSubscription = require('./EventSubscription');
11+
12+
/**
13+
* EmitterSubscription represents a subscription with listener and context data.
14+
*/
15+
class EmitterSubscription extends EventSubscription {
16+
17+
/**
18+
* @param {EventEmitter} emitter - The event emitter that registered this
19+
* subscription
20+
* @param {EventSubscriptionVendor} subscriber - The subscriber that controls
21+
* this subscription
22+
* @param {function} listener - Function to invoke when the specified event is
23+
* emitted
24+
* @param {*} context - Optional context object to use when invoking the
25+
* listener
26+
*/
27+
constructor(emitter, subscriber, listener, context) {
28+
super(subscriber);
29+
this.emitter = emitter;
30+
this.listener = listener;
31+
this.context = context;
32+
}
33+
34+
/**
35+
* Removes this subscription from the emitter that registered it.
36+
* Note: we're overriding the `remove()` method of EventSubscription here
37+
* but deliberately not calling `super.remove()` as the responsibility
38+
* for removing the subscription lies with the EventEmitter.
39+
*/
40+
remove() {
41+
this.emitter.removeSubscription(this);
42+
}
43+
}
44+
45+
module.exports = EmitterSubscription;
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
const EmitterSubscription = require('./EmitterSubscription');
11+
const EventSubscriptionVendor = require('./EventSubscriptionVendor');
12+
const invariant = require('invariant');
13+
14+
/**
15+
* @class EventEmitter
16+
* @description
17+
* An EventEmitter is responsible for managing a set of listeners and publishing
18+
* events to them when it is told that such events happened. In addition to the
19+
* data for the given event it also sends a event control object which allows
20+
* the listeners/handlers to prevent the default behavior of the given event.
21+
*
22+
* The emitter is designed to be generic enough to support all the different
23+
* contexts in which one might want to emit events. It is a simple multicast
24+
* mechanism on top of which extra functionality can be composed. For example, a
25+
* more advanced emitter may use an EventHolder and EventFactory.
26+
*/
27+
class EventEmitter {
28+
/**
29+
* @constructor
30+
*
31+
* @param {EventSubscriptionVendor} subscriber - Optional subscriber instance
32+
* to use. If omitted, a new subscriber will be created for the emitter.
33+
*/
34+
constructor(subscriber) {
35+
this._subscriber = subscriber || new EventSubscriptionVendor();
36+
}
37+
38+
/**
39+
* Adds a listener to be invoked when events of the specified type are
40+
* emitted. An optional calling context may be provided. The data arguments
41+
* emitted will be passed to the listener function.
42+
*
43+
* TODO: Annotate the listener arg's type. This is tricky because listeners
44+
* can be invoked with varargs.
45+
*
46+
* @param {string} eventType - Name of the event to listen to
47+
* @param {function} listener - Function to invoke when the specified event is
48+
* emitted
49+
* @param {*} context - Optional context object to use when invoking the
50+
* listener
51+
*/
52+
addListener(eventType, listener, context) {
53+
54+
return (this._subscriber.addSubscription(
55+
eventType,
56+
new EmitterSubscription(this, this._subscriber, listener, context)
57+
));
58+
}
59+
60+
/**
61+
* Similar to addListener, except that the listener is removed after it is
62+
* invoked once.
63+
*
64+
* @param {string} eventType - Name of the event to listen to
65+
* @param {function} listener - Function to invoke only once when the
66+
* specified event is emitted
67+
* @param {*} context - Optional context object to use when invoking the
68+
* listener
69+
*/
70+
once(eventType, listener, context) {
71+
return this.addListener(eventType, (...args) => {
72+
this.removeCurrentListener();
73+
listener.apply(context, args);
74+
});
75+
}
76+
77+
/**
78+
* Removes all of the registered listeners, including those registered as
79+
* listener maps.
80+
*
81+
* @param {?string} eventType - Optional name of the event whose registered
82+
* listeners to remove
83+
*/
84+
removeAllListeners(eventType) {
85+
this._subscriber.removeAllSubscriptions(eventType);
86+
}
87+
88+
/**
89+
* Provides an API that can be called during an eventing cycle to remove the
90+
* last listener that was invoked. This allows a developer to provide an event
91+
* object that can remove the listener (or listener map) during the
92+
* invocation.
93+
*
94+
* If it is called when not inside of an emitting cycle it will throw.
95+
*
96+
* @throws {Error} When called not during an eventing cycle
97+
*
98+
* @example
99+
* var subscription = emitter.addListenerMap({
100+
* someEvent: function(data, event) {
101+
* console.log(data);
102+
* emitter.removeCurrentListener();
103+
* }
104+
* });
105+
*
106+
* emitter.emit('someEvent', 'abc'); // logs 'abc'
107+
* emitter.emit('someEvent', 'def'); // does not log anything
108+
*/
109+
removeCurrentListener() {
110+
invariant(
111+
!!this._currentSubscription,
112+
'Not in an emitting cycle; there is no current subscription'
113+
);
114+
this.removeSubscription(this._currentSubscription);
115+
}
116+
117+
/**
118+
* Removes a specific subscription. Called by the `remove()` method of the
119+
* subscription itself to ensure any necessary cleanup is performed.
120+
*/
121+
removeSubscription(subscription) {
122+
invariant(
123+
subscription.emitter === this,
124+
'Subscription does not belong to this emitter.'
125+
);
126+
this._subscriber.removeSubscription(subscription);
127+
}
128+
129+
/**
130+
* Returns an array of listeners that are currently registered for the given
131+
* event.
132+
*
133+
* @param {string} eventType - Name of the event to query
134+
* @returns {array}
135+
*/
136+
listeners(eventType) {
137+
const subscriptions = (this._subscriber.getSubscriptionsForType(eventType));
138+
return subscriptions ? subscriptions.map(s => subscription.listener) : [];
139+
}
140+
141+
/**
142+
* Emits an event of the given type with the given data. All handlers of that
143+
* particular type will be notified.
144+
*
145+
* @param {string} eventType - Name of the event to emit
146+
* @param {...*} Arbitrary arguments to be passed to each registered listener
147+
*
148+
* @example
149+
* emitter.addListener('someEvent', function(message) {
150+
* console.log(message);
151+
* });
152+
*
153+
* emitter.emit('someEvent', 'abc'); // logs 'abc'
154+
*/
155+
emit(eventType) {
156+
const subscriptions = (this._subscriber.getSubscriptionsForType(eventType));
157+
if (subscriptions) {
158+
for (let i = 0, l = subscriptions.length; i < l; i++) {
159+
const subscription = subscriptions[i];
160+
161+
// The subscription may have been removed during this event loop.
162+
if (subscription) {
163+
this._currentSubscription = subscription;
164+
subscription.listener.apply(
165+
subscription.context,
166+
Array.prototype.slice.call(arguments, 1)
167+
);
168+
}
169+
}
170+
this._currentSubscription = null;
171+
}
172+
}
173+
174+
/**
175+
* Removes the given listener for event of specific type.
176+
*
177+
* @param {string} eventType - Name of the event to emit
178+
* @param {function} listener - Function to invoke when the specified event is
179+
* emitted
180+
*
181+
* @example
182+
* emitter.removeListener('someEvent', function(message) {
183+
* console.log(message);
184+
* }); // removes the listener if already registered
185+
*
186+
*/
187+
removeListener(eventType, listener) {
188+
const subscriptions = (this._subscriber.getSubscriptionsForType(eventType));
189+
if (subscriptions) {
190+
for (let i = 0, l = subscriptions.length; i < l; i++) {
191+
const subscription = subscriptions[i];
192+
193+
// The subscription may have been removed during this event loop.
194+
// its listener matches the listener in method parameters
195+
if (subscription && subscription.listener === listener) {
196+
subscription.remove();
197+
}
198+
}
199+
}
200+
}
201+
}
202+
203+
module.exports = EventEmitter;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
/**
11+
* EventSubscription represents a subscription to a particular event. It can
12+
* remove its own subscription.
13+
*/
14+
class EventSubscription {
15+
/**
16+
* @param {EventSubscriptionVendor} subscriber the subscriber that controls
17+
* this subscription.
18+
*/
19+
constructor(subscriber) {
20+
this.subscriber = subscriber;
21+
}
22+
23+
/**
24+
* Removes this subscription from the subscriber that controls it.
25+
*/
26+
remove() {
27+
this.subscriber.removeSubscription(this);
28+
}
29+
}
30+
31+
module.exports = EventSubscription;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
const invariant = require('invariant');
11+
12+
/**
13+
* EventSubscriptionVendor stores a set of EventSubscriptions that are
14+
* subscribed to a particular event type.
15+
*/
16+
class EventSubscriptionVendor {
17+
18+
constructor() {
19+
this._subscriptionsForType = {};
20+
this._currentSubscription = null;
21+
}
22+
23+
/**
24+
* Adds a subscription keyed by an event type.
25+
*
26+
* @param {string} eventType
27+
* @param {EventSubscription} subscription
28+
*/
29+
addSubscription(eventType, subscription) {
30+
/* eslint-disable no-param-reassign */
31+
invariant(
32+
subscription.subscriber === this,
33+
'The subscriber of the subscription is incorrectly set.');
34+
if (!this._subscriptionsForType[eventType]) {
35+
this._subscriptionsForType[eventType] = [];
36+
}
37+
const key = this._subscriptionsForType[eventType].length;
38+
this._subscriptionsForType[eventType].push(subscription);
39+
subscription.eventType = eventType;
40+
subscription.key = key;
41+
return subscription;
42+
}
43+
44+
/**
45+
* Removes a bulk set of the subscriptions.
46+
*
47+
* @param {?string} eventType - Optional name of the event type whose
48+
* registered supscriptions to remove, if null remove all subscriptions.
49+
*/
50+
removeAllSubscriptions(eventType) {
51+
if (eventType === undefined) {
52+
this._subscriptionsForType = {};
53+
} else {
54+
delete this._subscriptionsForType[eventType];
55+
}
56+
}
57+
58+
/**
59+
* Removes a specific subscription. Instead of calling this function, call
60+
* `subscription.remove()` directly.
61+
*
62+
* @param {object} subscription
63+
*/
64+
removeSubscription(subscription) {
65+
const eventType = subscription.eventType;
66+
const key = subscription.key;
67+
68+
const subscriptionsForType = this._subscriptionsForType[eventType];
69+
if (subscriptionsForType) {
70+
delete subscriptionsForType[key];
71+
}
72+
}
73+
74+
/**
75+
* Returns the array of subscriptions that are currently registered for the
76+
* given event type.
77+
*
78+
* Note: This array can be potentially sparse as subscriptions are deleted
79+
* from it when they are removed.
80+
*
81+
* TODO: This returns a nullable array. wat?
82+
*
83+
* @param {string} eventType
84+
* @returns {?array}
85+
*/
86+
getSubscriptionsForType(eventType) {
87+
return this._subscriptionsForType[eventType];
88+
}
89+
}
90+
91+
module.exports = EventSubscriptionVendor;

0 commit comments

Comments
 (0)