@samitouri / QOS-React / commits / 1890159a5d

Separate SyntheticEvent constructors to prevent deopts (#19907)

* Remove arguments from hot path * Make SyntheticEvent subtypes monomorphic * Maybe fix Flow?

Dan Abramov committed Sep 25, 2020 at 11:31 UTC 1890159a5d362e76ee949dfcace1ccb9677abc45
4 files changed +191 -172
packages/react-dom/src/events/SyntheticEvent.js
+156 -127
@@ -15,21 +15,6 @@ type EventInterfaceType = {
15 [propName: string]: 0 | ((event: {[propName: string]: mixed}) => mixed),
16 };
17
18 -/**
19 - * @interface Event
20 - * @see http://www.w3.org/TR/DOM-Level-3-Events/
21 - */
22 -const EventInterface: EventInterfaceType = {
23 - eventPhase: 0,
24 - bubbles: 0,
25 - cancelable: 0,
26 - timeStamp: function(event) {
27 - return event.timeStamp || Date.now();
28 - },
29 - defaultPrevented: 0,
30 - isTrusted: 0,
31 -};
32 -
18 function functionThatReturnsTrue() {
19 return true;
20 }
@@ -38,119 +23,140 @@ function functionThatReturnsFalse() {
23 return false;
24 }
25
41 -/**
42 - * Synthetic events are dispatched by event plugins, typically in response to a
43 - * top-level event delegation handler.
44 - *
45 - * These systems should generally use pooling to reduce the frequency of garbage
46 - * collection. The system should check `isPersistent` to determine whether the
47 - * event should be released into the pool after being dispatched. Users that
48 - * need a persisted event should invoke `persist`.
49 - *
50 - * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
51 - * normalizing browser quirks. Subclasses do not necessarily have to implement a
52 - * DOM interface; custom application-specific events can also subclass this.
53 - */
54 -export function SyntheticEvent(
55 - reactName: string | null,
56 - reactEventType: string,
57 - targetInst: Fiber,
58 - nativeEvent: {[propName: string]: mixed},
59 - nativeEventTarget: null | EventTarget,
60 - Interface: EventInterfaceType = EventInterface,
61 -) {
62 - this._reactName = reactName;
63 - this._targetInst = targetInst;
64 - this.type = reactEventType;
65 - this.nativeEvent = nativeEvent;
66 - this.target = nativeEventTarget;
67 - this.currentTarget = null;
68 -
69 - for (const propName in Interface) {
70 - if (!Interface.hasOwnProperty(propName)) {
71 - continue;
26 +// This is intentionally a factory so that we have different returned constructors.
27 +// If we had a single constructor, it would be megamorphic and engines would deopt.
28 +function createSyntheticEvent(Interface: EventInterfaceType) {
29 + /**
30 + * Synthetic events are dispatched by event plugins, typically in response to a
31 + * top-level event delegation handler.
32 + *
33 + * These systems should generally use pooling to reduce the frequency of garbage
34 + * collection. The system should check `isPersistent` to determine whether the
35 + * event should be released into the pool after being dispatched. Users that
36 + * need a persisted event should invoke `persist`.
37 + *
38 + * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
39 + * normalizing browser quirks. Subclasses do not necessarily have to implement a
40 + * DOM interface; custom application-specific events can also subclass this.
41 + */
42 + function SyntheticBaseEvent(
43 + reactName: string | null,
44 + reactEventType: string,
45 + targetInst: Fiber,
46 + nativeEvent: {[propName: string]: mixed},
47 + nativeEventTarget: null | EventTarget,
48 + ) {
49 + this._reactName = reactName;
50 + this._targetInst = targetInst;
51 + this.type = reactEventType;
52 + this.nativeEvent = nativeEvent;
53 + this.target = nativeEventTarget;
54 + this.currentTarget = null;
55 +
56 + for (const propName in Interface) {
57 + if (!Interface.hasOwnProperty(propName)) {
58 + continue;
59 + }
60 + const normalize = Interface[propName];
61 + if (normalize) {
62 + this[propName] = normalize(nativeEvent);
63 + } else {
64 + this[propName] = nativeEvent[propName];
65 + }
66 }
73 - const normalize = Interface[propName];
74 - if (normalize) {
75 - this[propName] = normalize(nativeEvent);
67 +
68 + const defaultPrevented =
69 + nativeEvent.defaultPrevented != null
70 + ? nativeEvent.defaultPrevented
71 + : nativeEvent.returnValue === false;
72 + if (defaultPrevented) {
73 + this.isDefaultPrevented = functionThatReturnsTrue;
74 } else {
77 - this[propName] = nativeEvent[propName];
75 + this.isDefaultPrevented = functionThatReturnsFalse;
76 }
77 + this.isPropagationStopped = functionThatReturnsFalse;
78 + return this;
79 }
80
81 - const defaultPrevented =
82 - nativeEvent.defaultPrevented != null
83 - ? nativeEvent.defaultPrevented
84 - : nativeEvent.returnValue === false;
85 - if (defaultPrevented) {
86 - this.isDefaultPrevented = functionThatReturnsTrue;
87 - } else {
88 - this.isDefaultPrevented = functionThatReturnsFalse;
89 - }
90 - this.isPropagationStopped = functionThatReturnsFalse;
91 - return this;
81 + Object.assign(SyntheticBaseEvent.prototype, {
82 + preventDefault: function() {
83 + this.defaultPrevented = true;
84 + const event = this.nativeEvent;
85 + if (!event) {
86 + return;
87 + }
88 +
89 + if (event.preventDefault) {
90 + event.preventDefault();
91 + // $FlowFixMe - flow is not aware of `unknown` in IE
92 + } else if (typeof event.returnValue !== 'unknown') {
93 + event.returnValue = false;
94 + }
95 + this.isDefaultPrevented = functionThatReturnsTrue;
96 + },
97 +
98 + stopPropagation: function() {
99 + const event = this.nativeEvent;
100 + if (!event) {
101 + return;
102 + }
103 +
104 + if (event.stopPropagation) {
105 + event.stopPropagation();
106 + // $FlowFixMe - flow is not aware of `unknown` in IE
107 + } else if (typeof event.cancelBubble !== 'unknown') {
108 + // The ChangeEventPlugin registers a "propertychange" event for
109 + // IE. This event does not support bubbling or cancelling, and
110 + // any references to cancelBubble throw "Member not found". A
111 + // typeof check of "unknown" circumvents this issue (and is also
112 + // IE specific).
113 + event.cancelBubble = true;
114 + }
115 +
116 + this.isPropagationStopped = functionThatReturnsTrue;
117 + },
118 +
119 + /**
120 + * We release all dispatched `SyntheticEvent`s after each event loop, adding
121 + * them back into the pool. This allows a way to hold onto a reference that
122 + * won't be added back into the pool.
123 + */
124 + persist: function() {
125 + // Modern event system doesn't use pooling.
126 + },
127 +
128 + /**
129 + * Checks if this event should be released back into the pool.
130 + *
131 + * @return {boolean} True if this should not be released, false otherwise.
132 + */
133 + isPersistent: functionThatReturnsTrue,
134 + });
135 + return SyntheticBaseEvent;
136 }
137
94 -Object.assign(SyntheticEvent.prototype, {
95 - preventDefault: function() {
96 - this.defaultPrevented = true;
97 - const event = this.nativeEvent;
98 - if (!event) {
99 - return;
100 - }
101 -
102 - if (event.preventDefault) {
103 - event.preventDefault();
104 - // $FlowFixMe - flow is not aware of `unknown` in IE
105 - } else if (typeof event.returnValue !== 'unknown') {
106 - event.returnValue = false;
107 - }
108 - this.isDefaultPrevented = functionThatReturnsTrue;
109 - },
110 -
111 - stopPropagation: function() {
112 - const event = this.nativeEvent;
113 - if (!event) {
114 - return;
115 - }
116 -
117 - if (event.stopPropagation) {
118 - event.stopPropagation();
119 - // $FlowFixMe - flow is not aware of `unknown` in IE
120 - } else if (typeof event.cancelBubble !== 'unknown') {
121 - // The ChangeEventPlugin registers a "propertychange" event for
122 - // IE. This event does not support bubbling or cancelling, and
123 - // any references to cancelBubble throw "Member not found". A
124 - // typeof check of "unknown" circumvents this issue (and is also
125 - // IE specific).
126 - event.cancelBubble = true;
127 - }
128 -
129 - this.isPropagationStopped = functionThatReturnsTrue;
130 - },
131 -
132 - /**
133 - * We release all dispatched `SyntheticEvent`s after each event loop, adding
134 - * them back into the pool. This allows a way to hold onto a reference that
135 - * won't be added back into the pool.
136 - */
137 - persist: function() {
138 - // Modern event system doesn't use pooling.
138 +/**
139 + * @interface Event
140 + * @see http://www.w3.org/TR/DOM-Level-3-Events/
141 + */
142 +const EventInterface = {
143 + eventPhase: 0,
144 + bubbles: 0,
145 + cancelable: 0,
146 + timeStamp: function(event) {
147 + return event.timeStamp || Date.now();
148 },
149 + defaultPrevented: 0,
150 + isTrusted: 0,
151 +};
152 +export const SyntheticEvent = createSyntheticEvent(EventInterface);
153
141 - /**
142 - * Checks if this event should be released back into the pool.
143 - *
144 - * @return {boolean} True if this should not be released, false otherwise.
145 - */
146 - isPersistent: functionThatReturnsTrue,
147 -});
148 -
149 -export const UIEventInterface: EventInterfaceType = {
154 +const UIEventInterface: EventInterfaceType = {
155 ...EventInterface,
156 view: 0,
157 detail: 0,
158 };
159 +export const SyntheticUIEvent = createSyntheticEvent(UIEventInterface);
160
161 let lastMovementX;
162 let lastMovementY;
@@ -173,7 +179,7 @@ function updateMouseMovementPolyfillState(event) {
179 * @interface MouseEvent
180 * @see http://www.w3.org/TR/DOM-Level-3-Events/
181 */
176 -export const MouseEventInterface: EventInterfaceType = {
182 +const MouseEventInterface: EventInterfaceType = {
183 ...UIEventInterface,
184 screenX: 0,
185 screenY: 0,
@@ -213,42 +219,48 @@ export const MouseEventInterface: EventInterfaceType = {
219 return lastMovementY;
220 },
221 };
222 +export const SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface);
223
224 /**
225 * @interface DragEvent
226 * @see http://www.w3.org/TR/DOM-Level-3-Events/
227 */
221 -export const DragEventInterface: EventInterfaceType = {
228 +const DragEventInterface: EventInterfaceType = {
229 ...MouseEventInterface,
230 dataTransfer: 0,
231 };
232 +export const SyntheticDragEvent = createSyntheticEvent(DragEventInterface);
233
234 /**
235 * @interface FocusEvent
236 * @see http://www.w3.org/TR/DOM-Level-3-Events/
237 */
230 -export const FocusEventInterface: EventInterfaceType = {
238 +const FocusEventInterface: EventInterfaceType = {
239 ...UIEventInterface,
240 relatedTarget: 0,
241 };
242 +export const SyntheticFocusEvent = createSyntheticEvent(FocusEventInterface);
243
244 /**
245 * @interface Event
246 * @see http://www.w3.org/TR/css3-animations/#AnimationEvent-interface
247 * @see https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent
248 */
240 -export const AnimationEventInterface: EventInterfaceType = {
249 +const AnimationEventInterface: EventInterfaceType = {
250 ...EventInterface,
251 animationName: 0,
252 elapsedTime: 0,
253 pseudoElement: 0,
254 };
255 +export const SyntheticAnimationEvent = createSyntheticEvent(
256 + AnimationEventInterface,
257 +);
258
259 /**
260 * @interface Event
261 * @see http://www.w3.org/TR/clipboard-apis/
262 */
251 -export const ClipboardEventInterface: EventInterfaceType = {
263 +const ClipboardEventInterface: EventInterfaceType = {
264 ...EventInterface,
265 clipboardData: function(event) {
266 return 'clipboardData' in event
@@ -256,15 +268,21 @@ export const ClipboardEventInterface: EventInterfaceType = {
268 : window.clipboardData;
269 },
270 };
271 +export const SyntheticClipboardEvent = createSyntheticEvent(
272 + ClipboardEventInterface,
273 +);
274
275 /**
276 * @interface Event
277 * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents
278 */
264 -export const CompositionEventInterface: EventInterfaceType = {
279 +const CompositionEventInterface: EventInterfaceType = {
280 ...EventInterface,
281 data: 0,
282 };
283 +export const SyntheticCompositionEvent = createSyntheticEvent(
284 + CompositionEventInterface,
285 +);
286
287 /**
288 * @interface Event
@@ -272,7 +290,7 @@ export const CompositionEventInterface: EventInterfaceType = {
290 * /#events-inputevents
291 */
292 // Happens to share the same list for now.
275 -export const InputEventInterface: EventInterfaceType = CompositionEventInterface;
293 +export const SyntheticInputEvent = SyntheticCompositionEvent;
294
295 /**
296 * Normalization of deprecated HTML5 `key` values
@@ -402,7 +420,7 @@ function getEventModifierState(nativeEvent) {
420 * @interface KeyboardEvent
421 * @see http://www.w3.org/TR/DOM-Level-3-Events/
422 */
405 -export const KeyboardEventInterface = {
423 +const KeyboardEventInterface = {
424 ...UIEventInterface,
425 key: getEventKey,
426 code: 0,
@@ -451,12 +469,15 @@ export const KeyboardEventInterface = {
469 return 0;
470 },
471 };
472 +export const SyntheticKeyboardEvent = createSyntheticEvent(
473 + KeyboardEventInterface,
474 +);
475
476 /**
477 * @interface PointerEvent
478 * @see http://www.w3.org/TR/pointerevents/
479 */
459 -export const PointerEventInterface = {
480 +const PointerEventInterface = {
481 ...MouseEventInterface,
482 pointerId: 0,
483 width: 0,
@@ -469,12 +490,15 @@ export const PointerEventInterface = {
490 pointerType: 0,
491 isPrimary: 0,
492 };
493 +export const SyntheticPointerEvent = createSyntheticEvent(
494 + PointerEventInterface,
495 +);
496
497 /**
498 * @interface TouchEvent
499 * @see http://www.w3.org/TR/touch-events/
500 */
477 -export const TouchEventInterface = {
501 +const TouchEventInterface = {
502 ...UIEventInterface,
503 touches: 0,
504 targetTouches: 0,
@@ -485,24 +509,28 @@ export const TouchEventInterface = {
509 shiftKey: 0,
510 getModifierState: getEventModifierState,
511 };
512 +export const SyntheticTouchEvent = createSyntheticEvent(TouchEventInterface);
513
514 /**
515 * @interface Event
516 * @see http://www.w3.org/TR/2009/WD-css3-transitions-20090320/#transition-events-
517 * @see https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent
518 */
494 -export const TransitionEventInterface = {
519 +const TransitionEventInterface = {
520 ...EventInterface,
521 propertyName: 0,
522 elapsedTime: 0,
523 pseudoElement: 0,
524 };
525 +export const SyntheticTransitionEvent = createSyntheticEvent(
526 + TransitionEventInterface,
527 +);
528
529 /**
530 * @interface WheelEvent
531 * @see http://www.w3.org/TR/DOM-Level-3-Events/
532 */
505 -export const WheelEventInterface = {
533 +const WheelEventInterface = {
534 ...MouseEventInterface,
535 deltaX(event) {
536 return 'deltaX' in event
@@ -531,3 +559,4 @@ export const WheelEventInterface = {
559 // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.
560 deltaMode: 0,
561 };
562 +export const SyntheticWheelEvent = createSyntheticEvent(WheelEventInterface);
packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js
+4 -7
@@ -22,9 +22,8 @@ import {
22 reset as FallbackCompositionStateReset,
23 } from '../FallbackCompositionState';
24 import {
25 - CompositionEventInterface,
26 - InputEventInterface,
27 - SyntheticEvent,
25 + SyntheticCompositionEvent,
26 + SyntheticInputEvent,
27 } from '../SyntheticEvent';
28 import {accumulateTwoPhaseListeners} from '../DOMPluginEventSystem';
29
@@ -227,13 +226,12 @@ function extractCompositionEvent(
226 }
227 }
228
230 - const event = new SyntheticEvent(
229 + const event = new SyntheticCompositionEvent(
230 eventType,
231 domEventName,
232 null,
233 nativeEvent,
234 nativeEventTarget,
236 - CompositionEventInterface,
235 );
236 accumulateTwoPhaseListeners(targetInst, dispatchQueue, event);
237
@@ -396,13 +394,12 @@ function extractBeforeInputEvent(
394 return null;
395 }
396
399 - const event = new SyntheticEvent(
397 + const event = new SyntheticInputEvent(
398 'onBeforeInput',
399 'beforeinput',
400 null,
401 nativeEvent,
402 nativeEventTarget,
405 - InputEventInterface,
403 );
404 accumulateTwoPhaseListeners(targetInst, dispatchQueue, event);
405 event.data = chars;
packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js
+5 -11
@@ -14,11 +14,7 @@ import type {EventSystemFlags} from '../EventSystemFlags';
14
15 import {registerDirectEvent} from '../EventRegistry';
16 import {IS_REPLAYED} from 'react-dom/src/events/EventSystemFlags';
17 -import {
18 - SyntheticEvent,
19 - MouseEventInterface,
20 - PointerEventInterface,
21 -} from '../SyntheticEvent';
17 +import {SyntheticMouseEvent, SyntheticPointerEvent} from '../SyntheticEvent';
18 import {
19 getClosestInstanceFromNode,
20 getNodeFromInstance,
@@ -123,12 +119,12 @@ function extractEvents(
119 return;
120 }
121
126 - let eventInterface = MouseEventInterface;
122 + let SyntheticEventCtor = SyntheticMouseEvent;
123 let leaveEventType = 'onMouseLeave';
124 let enterEventType = 'onMouseEnter';
125 let eventTypePrefix = 'mouse';
126 if (domEventName === 'pointerout' || domEventName === 'pointerover') {
131 - eventInterface = PointerEventInterface;
127 + SyntheticEventCtor = SyntheticPointerEvent;
128 leaveEventType = 'onPointerLeave';
129 enterEventType = 'onPointerEnter';
130 eventTypePrefix = 'pointer';
@@ -137,13 +133,12 @@ function extractEvents(
133 const fromNode = from == null ? win : getNodeFromInstance(from);
134 const toNode = to == null ? win : getNodeFromInstance(to);
135
140 - const leave = new SyntheticEvent(
136 + const leave = new SyntheticEventCtor(
137 leaveEventType,
138 eventTypePrefix + 'leave',
139 from,
140 nativeEvent,
141 nativeEventTarget,
146 - eventInterface,
142 );
143 leave.target = fromNode;
144 leave.relatedTarget = toNode;
@@ -154,13 +149,12 @@ function extractEvents(
149 // the first ancestor. Next time, we will ignore the event.
150 const nativeTargetInst = getClosestInstanceFromNode((nativeEventTarget: any));
151 if (nativeTargetInst === targetInst) {
157 - const enterEvent: KnownReactSyntheticEvent = new SyntheticEvent(
152 + const enterEvent: KnownReactSyntheticEvent = new SyntheticEventCtor(
153 enterEventType,
154 eventTypePrefix + 'enter',
155 to,
156 nativeEvent,
157 nativeEventTarget,
163 - eventInterface,
158 );
159 enterEvent.target = toNode;
160 enterEvent.relatedTarget = fromNode;
packages/react-dom/src/events/plugins/SimpleEventPlugin.js
+26 -27
@@ -15,17 +15,17 @@ import type {EventSystemFlags} from '../EventSystemFlags';
15
16 import {
17 SyntheticEvent,
18 - AnimationEventInterface,
19 - ClipboardEventInterface,
20 - FocusEventInterface,
21 - KeyboardEventInterface,
22 - MouseEventInterface,
23 - PointerEventInterface,
24 - DragEventInterface,
25 - TouchEventInterface,
26 - TransitionEventInterface,
27 - UIEventInterface,
28 - WheelEventInterface,
18 + SyntheticKeyboardEvent,
19 + SyntheticFocusEvent,
20 + SyntheticMouseEvent,
21 + SyntheticDragEvent,
22 + SyntheticTouchEvent,
23 + SyntheticAnimationEvent,
24 + SyntheticTransitionEvent,
25 + SyntheticUIEvent,
26 + SyntheticWheelEvent,
27 + SyntheticClipboardEvent,
28 + SyntheticPointerEvent,
29 } from '../../events/SyntheticEvent';
30
31 import {
@@ -62,7 +62,7 @@ function extractEvents(
62 if (reactName === undefined) {
63 return;
64 }
65 - let EventInterface;
65 + let SyntheticEventCtor = SyntheticEvent;
66 let reactEventType = domEventName;
67 switch (domEventName) {
68 case 'keypress':
@@ -75,19 +75,19 @@ function extractEvents(
75 /* falls through */
76 case 'keydown':
77 case 'keyup':
78 - EventInterface = KeyboardEventInterface;
78 + SyntheticEventCtor = SyntheticKeyboardEvent;
79 break;
80 case 'focusin':
81 reactEventType = 'focus';
82 - EventInterface = FocusEventInterface;
82 + SyntheticEventCtor = SyntheticFocusEvent;
83 break;
84 case 'focusout':
85 reactEventType = 'blur';
86 - EventInterface = FocusEventInterface;
86 + SyntheticEventCtor = SyntheticFocusEvent;
87 break;
88 case 'beforeblur':
89 case 'afterblur':
90 - EventInterface = FocusEventInterface;
90 + SyntheticEventCtor = SyntheticFocusEvent;
91 break;
92 case 'click':
93 // Firefox creates a click event on right mouse clicks. This removes the
@@ -106,7 +106,7 @@ function extractEvents(
106 case 'mouseout':
107 case 'mouseover':
108 case 'contextmenu':
109 - EventInterface = MouseEventInterface;
109 + SyntheticEventCtor = SyntheticMouseEvent;
110 break;
111 case 'drag':
112 case 'dragend':
@@ -116,32 +116,32 @@ function extractEvents(
116 case 'dragover':
117 case 'dragstart':
118 case 'drop':
119 - EventInterface = DragEventInterface;
119 + SyntheticEventCtor = SyntheticDragEvent;
120 break;
121 case 'touchcancel':
122 case 'touchend':
123 case 'touchmove':
124 case 'touchstart':
125 - EventInterface = TouchEventInterface;
125 + SyntheticEventCtor = SyntheticTouchEvent;
126 break;
127 case ANIMATION_END:
128 case ANIMATION_ITERATION:
129 case ANIMATION_START:
130 - EventInterface = AnimationEventInterface;
130 + SyntheticEventCtor = SyntheticAnimationEvent;
131 break;
132 case TRANSITION_END:
133 - EventInterface = TransitionEventInterface;
133 + SyntheticEventCtor = SyntheticTransitionEvent;
134 break;
135 case 'scroll':
136 - EventInterface = UIEventInterface;
136 + SyntheticEventCtor = SyntheticUIEvent;
137 break;
138 case 'wheel':
139 - EventInterface = WheelEventInterface;
139 + SyntheticEventCtor = SyntheticWheelEvent;
140 break;
141 case 'copy':
142 case 'cut':
143 case 'paste':
144 - EventInterface = ClipboardEventInterface;
144 + SyntheticEventCtor = SyntheticClipboardEvent;
145 break;
146 case 'gotpointercapture':
147 case 'lostpointercapture':
@@ -151,19 +151,18 @@ function extractEvents(
151 case 'pointerout':
152 case 'pointerover':
153 case 'pointerup':
154 - EventInterface = PointerEventInterface;
154 + SyntheticEventCtor = SyntheticPointerEvent;
155 break;
156 default:
157 // Unknown event. This is used by createEventHandle.
158 break;
159 }
160 - const event = new SyntheticEvent(
160 + const event = new SyntheticEventCtor(
161 reactName,
162 reactEventType,
163 null,
164 nativeEvent,
165 nativeEventTarget,
166 - EventInterface,
166 );
167
168 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;