main
js 362 lines 9.43 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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
8 /* eslint valid-typeof: 0 */
9
10 import assign from 'shared/assign';
11
12 const EVENT_POOL_SIZE = 10;
13
14 let currentTimeStamp = () => {
15 // Lazily define the function based on the existence of performance.now()
16 if (
17 typeof performance === 'object' &&
18 performance !== null &&
19 typeof performance.now === 'function'
20 ) {
21 currentTimeStamp = () => performance.now();
22 } else {
23 currentTimeStamp = () => Date.now();
24 }
25
26 return currentTimeStamp();
27 };
28
29 /**
30 * @interface Event
31 * @see http://www.w3.org/TR/DOM-Level-3-Events/
32 */
33 const EventInterface = {
34 type: null,
35 target: null,
36 // currentTarget is set when dispatching; no use in copying it here
37 currentTarget: function () {
38 return null;
39 },
40 eventPhase: null,
41 bubbles: null,
42 cancelable: null,
43 timeStamp: function (event) {
44 return event.timeStamp || event.timestamp || currentTimeStamp();
45 },
46 defaultPrevented: null,
47 isTrusted: null,
48 };
49
50 function functionThatReturnsTrue() {
51 return true;
52 }
53
54 function functionThatReturnsFalse() {
55 return false;
56 }
57
58 /**
59 * Synthetic events are dispatched by event plugins, typically in response to a
60 * top-level event delegation handler.
61 *
62 * These systems should generally use pooling to reduce the frequency of garbage
63 * collection. The system should check `isPersistent` to determine whether the
64 * event should be released into the pool after being dispatched. Users that
65 * need a persisted event should invoke `persist`.
66 *
67 * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
68 * normalizing browser quirks. Subclasses do not necessarily have to implement a
69 * DOM interface; custom application-specific events can also subclass this.
70 *
71 * @param {object} dispatchConfig Configuration used to dispatch this event.
72 * @param {*} targetInst Marker identifying the event target.
73 * @param {object} nativeEvent Native browser event.
74 * @param {DOMEventTarget} nativeEventTarget Target node.
75 */
76 function SyntheticEvent(
77 dispatchConfig,
78 targetInst,
79 nativeEvent,
80 nativeEventTarget,
81 ) {
82 if (__DEV__) {
83 // these have a getter/setter for warnings
84 delete this.nativeEvent;
85 delete this.preventDefault;
86 delete this.stopPropagation;
87 delete this.isDefaultPrevented;
88 delete this.isPropagationStopped;
89 }
90
91 this.dispatchConfig = dispatchConfig;
92 this._targetInst = targetInst;
93 this.nativeEvent = nativeEvent;
94 this._dispatchListeners = null;
95 this._dispatchInstances = null;
96
97 const Interface = this.constructor.Interface;
98 for (const propName in Interface) {
99 if (!Interface.hasOwnProperty(propName)) {
100 continue;
101 }
102 if (__DEV__) {
103 delete this[propName]; // this has a getter/setter for warnings
104 }
105 const normalize = Interface[propName];
106 if (normalize) {
107 this[propName] = normalize(nativeEvent);
108 } else {
109 if (propName === 'target') {
110 this.target = nativeEventTarget;
111 } else {
112 this[propName] = nativeEvent[propName];
113 }
114 }
115 }
116
117 const defaultPrevented =
118 nativeEvent.defaultPrevented != null
119 ? nativeEvent.defaultPrevented
120 : nativeEvent.returnValue === false;
121 if (defaultPrevented) {
122 this.isDefaultPrevented = functionThatReturnsTrue;
123 } else {
124 this.isDefaultPrevented = functionThatReturnsFalse;
125 }
126 this.isPropagationStopped = functionThatReturnsFalse;
127 return this;
128 }
129
130 assign(SyntheticEvent.prototype, {
131 preventDefault: function () {
132 this.defaultPrevented = true;
133 const event = this.nativeEvent;
134 if (!event) {
135 return;
136 }
137
138 if (event.preventDefault) {
139 event.preventDefault();
140 } else if (typeof event.returnValue !== 'unknown') {
141 event.returnValue = false;
142 }
143 this.isDefaultPrevented = functionThatReturnsTrue;
144 },
145
146 stopPropagation: function () {
147 const event = this.nativeEvent;
148 if (!event) {
149 return;
150 }
151
152 if (event.stopPropagation) {
153 event.stopPropagation();
154 } else if (typeof event.cancelBubble !== 'unknown') {
155 // The ChangeEventPlugin registers a "propertychange" event for
156 // IE. This event does not support bubbling or cancelling, and
157 // any references to cancelBubble throw "Member not found". A
158 // typeof check of "unknown" circumvents this issue (and is also
159 // IE specific).
160 event.cancelBubble = true;
161 }
162
163 this.isPropagationStopped = functionThatReturnsTrue;
164 },
165
166 /**
167 * We release all dispatched `SyntheticEvent`s after each event loop, adding
168 * them back into the pool. This allows a way to hold onto a reference that
169 * won't be added back into the pool.
170 */
171 persist: function () {
172 this.isPersistent = functionThatReturnsTrue;
173 },
174
175 /**
176 * Checks if this event should be released back into the pool.
177 *
178 * @return {boolean} True if this should not be released, false otherwise.
179 */
180 isPersistent: functionThatReturnsFalse,
181
182 /**
183 * `PooledClass` looks for `destructor` on each instance it releases.
184 */
185 destructor: function () {
186 const Interface = this.constructor.Interface;
187 for (const propName in Interface) {
188 if (__DEV__) {
189 Object.defineProperty(
190 this,
191 propName,
192 getPooledWarningPropertyDefinition(propName, Interface[propName]),
193 );
194 } else {
195 this[propName] = null;
196 }
197 }
198 this.dispatchConfig = null;
199 this._targetInst = null;
200 this.nativeEvent = null;
201 this.isDefaultPrevented = functionThatReturnsFalse;
202 this.isPropagationStopped = functionThatReturnsFalse;
203 this._dispatchListeners = null;
204 this._dispatchInstances = null;
205 if (__DEV__) {
206 Object.defineProperty(
207 this,
208 'nativeEvent',
209 getPooledWarningPropertyDefinition('nativeEvent', null),
210 );
211 Object.defineProperty(
212 this,
213 'isDefaultPrevented',
214 getPooledWarningPropertyDefinition(
215 'isDefaultPrevented',
216 functionThatReturnsFalse,
217 ),
218 );
219 Object.defineProperty(
220 this,
221 'isPropagationStopped',
222 getPooledWarningPropertyDefinition(
223 'isPropagationStopped',
224 functionThatReturnsFalse,
225 ),
226 );
227 Object.defineProperty(
228 this,
229 'preventDefault',
230 getPooledWarningPropertyDefinition('preventDefault', () => {}),
231 );
232 Object.defineProperty(
233 this,
234 'stopPropagation',
235 getPooledWarningPropertyDefinition('stopPropagation', () => {}),
236 );
237 }
238 },
239 });
240
241 SyntheticEvent.Interface = EventInterface;
242
243 /**
244 * Helper to reduce boilerplate when creating subclasses.
245 */
246 SyntheticEvent.extend = function (Interface) {
247 const Super = this;
248
249 const E = function () {};
250 E.prototype = Super.prototype;
251 const prototype = new E();
252
253 function Class() {
254 return Super.apply(this, arguments);
255 }
256 assign(prototype, Class.prototype);
257 Class.prototype = prototype;
258 Class.prototype.constructor = Class;
259
260 Class.Interface = assign({}, Super.Interface, Interface);
261 Class.extend = Super.extend;
262 addEventPoolingTo(Class);
263
264 return Class;
265 };
266
267 addEventPoolingTo(SyntheticEvent);
268
269 /**
270 * Helper to nullify syntheticEvent instance properties when destructing
271 *
272 * @param {String} propName
273 * @param {?object} getVal
274 * @return {object} defineProperty object
275 */
276 function getPooledWarningPropertyDefinition(propName, getVal) {
277 function set(val) {
278 const action = isFunction ? 'setting the method' : 'setting the property';
279 warn(action, 'This is effectively a no-op');
280 return val;
281 }
282
283 function get() {
284 const action = isFunction
285 ? 'accessing the method'
286 : 'accessing the property';
287 const result = isFunction
288 ? 'This is a no-op function'
289 : 'This is set to null';
290 warn(action, result);
291 return getVal;
292 }
293
294 function warn(action, result) {
295 if (__DEV__) {
296 console.error(
297 "This synthetic event is reused for performance reasons. If you're seeing this, " +
298 "you're %s `%s` on a released/nullified synthetic event. %s. " +
299 'If you must keep the original synthetic event around, use event.persist(). ' +
300 'See https://react.dev/link/event-pooling for more information.',
301 action,
302 propName,
303 result,
304 );
305 }
306 }
307 const isFunction = typeof getVal === 'function';
308 return {
309 configurable: true,
310 set: set,
311 get: get,
312 };
313 }
314
315 function createOrGetPooledEvent(
316 dispatchConfig,
317 targetInst,
318 nativeEvent,
319 nativeInst,
320 ) {
321 const EventConstructor = this;
322 if (EventConstructor.eventPool.length) {
323 const instance = EventConstructor.eventPool.pop();
324 EventConstructor.call(
325 instance,
326 dispatchConfig,
327 targetInst,
328 nativeEvent,
329 nativeInst,
330 );
331 return instance;
332 }
333 return new EventConstructor(
334 dispatchConfig,
335 targetInst,
336 nativeEvent,
337 nativeInst,
338 );
339 }
340
341 function releasePooledEvent(event) {
342 const EventConstructor = this;
343
344 if (!(event instanceof EventConstructor)) {
345 throw new Error(
346 'Trying to release an event instance into a pool of a different type.',
347 );
348 }
349
350 event.destructor();
351 if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
352 EventConstructor.eventPool.push(event);
353 }
354 }
355
356 function addEventPoolingTo(EventConstructor) {
357 EventConstructor.getPooled = createOrGetPooledEvent;
358 EventConstructor.eventPool = [];
359 EventConstructor.release = releasePooledEvent;
360 }
361
362 export default SyntheticEvent;