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