UNPKG

7.57 kBJavaScriptView Raw
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'use strict';
13
14const EmitterSubscription = require('EmitterSubscription');
15const EventSubscriptionVendor = require('EventSubscriptionVendor');
16
17const invariant = require('invariant');
18
19const 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 */
34class 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
228module.exports = EventEmitter;