From 480626a9e920d5e04194c793a828318102ea4ff4 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 25 Sep 2020 13:33:28 +0100 Subject: [PATCH] Create Synthetic Events Lazily (#19909) --- .../src/events/DOMPluginEventSystem.js | 87 +++++++------------ .../events/plugins/BeforeInputEventPlugin.js | 57 ++++++------ .../src/events/plugins/ChangeEventPlugin.js | 19 ++-- .../src/events/plugins/SelectEventPlugin.js | 25 +++--- .../src/events/plugins/SimpleEventPlugin.js | 44 +++++++--- 5 files changed, 118 insertions(+), 114 deletions(-) diff --git a/packages/react-dom/src/events/DOMPluginEventSystem.js b/packages/react-dom/src/events/DOMPluginEventSystem.js index 3b702efc3cd1f..0b374b6e91d29 100644 --- a/packages/react-dom/src/events/DOMPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMPluginEventSystem.js @@ -709,31 +709,19 @@ function createDispatchListener( }; } -function createDispatchEntry( - event: ReactSyntheticEvent, - listeners: Array, -): DispatchEntry { - return { - event, - listeners, - }; -} - export function accumulateSinglePhaseListeners( targetFiber: Fiber | null, - dispatchQueue: DispatchQueue, - event: ReactSyntheticEvent, + reactName: string | null, + nativeEventType: string, inCapturePhase: boolean, accumulateTargetOnly: boolean, -): void { - const bubbleName = event._reactName; - const captureName = bubbleName !== null ? bubbleName + 'Capture' : null; - const reactEventName = inCapturePhase ? captureName : bubbleName; +): Array { + const captureName = reactName !== null ? reactName + 'Capture' : null; + const reactEventName = inCapturePhase ? captureName : reactName; const listeners: Array = []; let instance = targetFiber; let lastHostComponent = null; - const targetType = event.nativeEvent.type; // Accumulate all instances and listeners via the target -> root path. while (instance !== null) { @@ -749,7 +737,10 @@ export function accumulateSinglePhaseListeners( ); if (eventHandlerListeners !== null) { eventHandlerListeners.forEach(entry => { - if (entry.type === targetType && entry.capture === inCapturePhase) { + if ( + entry.type === nativeEventType && + entry.capture === inCapturePhase + ) { listeners.push( createDispatchListener( instance, @@ -785,7 +776,10 @@ export function accumulateSinglePhaseListeners( ); if (eventHandlerListeners !== null) { eventHandlerListeners.forEach(entry => { - if (entry.type === targetType && entry.capture === inCapturePhase) { + if ( + entry.type === nativeEventType && + entry.capture === inCapturePhase + ) { listeners.push( createDispatchListener( instance, @@ -805,9 +799,7 @@ export function accumulateSinglePhaseListeners( } instance = instance.return; } - if (listeners.length !== 0) { - dispatchQueue.push(createDispatchEntry(event, listeners)); - } + return listeners; } // We should only use this function for: @@ -819,11 +811,9 @@ export function accumulateSinglePhaseListeners( // phase event listeners (via emulation). export function accumulateTwoPhaseListeners( targetFiber: Fiber | null, - dispatchQueue: DispatchQueue, - event: ReactSyntheticEvent, -): void { - const bubbleName = event._reactName; - const captureName = bubbleName !== null ? bubbleName + 'Capture' : null; + reactName: string, +): Array { + const captureName = reactName + 'Capture'; const listeners: Array = []; let instance = targetFiber; @@ -833,29 +823,22 @@ export function accumulateTwoPhaseListeners( // Handle listeners that are on HostComponents (i.e.
) if (tag === HostComponent && stateNode !== null) { const currentTarget = stateNode; - // Standard React on* listeners, i.e. onClick prop - if (captureName !== null) { - const captureListener = getListener(instance, captureName); - if (captureListener != null) { - listeners.unshift( - createDispatchListener(instance, captureListener, currentTarget), - ); - } + const captureListener = getListener(instance, captureName); + if (captureListener != null) { + listeners.unshift( + createDispatchListener(instance, captureListener, currentTarget), + ); } - if (bubbleName !== null) { - const bubbleListener = getListener(instance, bubbleName); - if (bubbleListener != null) { - listeners.push( - createDispatchListener(instance, bubbleListener, currentTarget), - ); - } + const bubbleListener = getListener(instance, reactName); + if (bubbleListener != null) { + listeners.push( + createDispatchListener(instance, bubbleListener, currentTarget), + ); } } instance = instance.return; } - if (listeners.length !== 0) { - dispatchQueue.push(createDispatchEntry(event, listeners)); - } + return listeners; } function getParent(inst: Fiber | null): Fiber | null { @@ -956,7 +939,7 @@ function accumulateEnterLeaveListenersForEvent( instance = instance.return; } if (listeners.length !== 0) { - dispatchQueue.push(createDispatchEntry(event, listeners)); + dispatchQueue.push({event, listeners}); } } @@ -995,27 +978,23 @@ export function accumulateEnterLeaveTwoPhaseListeners( } export function accumulateEventHandleNonManagedNodeListeners( - dispatchQueue: DispatchQueue, - event: ReactSyntheticEvent, + reactEventType: DOMEventName, currentTarget: EventTarget, inCapturePhase: boolean, -): void { +): Array { const listeners: Array = []; const eventListeners = getEventHandlerListeners(currentTarget); if (eventListeners !== null) { - const targetType = ((event.type: any): DOMEventName); eventListeners.forEach(entry => { - if (entry.type === targetType && entry.capture === inCapturePhase) { + if (entry.type === reactEventType && entry.capture === inCapturePhase) { listeners.push( createDispatchListener(null, entry.callback, currentTarget), ); } }); } - if (listeners.length !== 0) { - dispatchQueue.push(createDispatchEntry(event, listeners)); - } + return listeners; } export function getListenerSetKey( diff --git a/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js b/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js index 1935d30170e51..5e413a3459e14 100644 --- a/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js +++ b/packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js @@ -226,23 +226,25 @@ function extractCompositionEvent( } } - const event = new SyntheticCompositionEvent( - eventType, - domEventName, - null, - nativeEvent, - nativeEventTarget, - ); - accumulateTwoPhaseListeners(targetInst, dispatchQueue, event); - - if (fallbackData) { - // Inject data generated from fallback path into the synthetic event. - // This matches the property of native CompositionEventInterface. - event.data = fallbackData; - } else { - const customData = getDataFromCustomEvent(nativeEvent); - if (customData !== null) { - event.data = customData; + const listeners = accumulateTwoPhaseListeners(targetInst, eventType); + if (listeners.length > 0) { + const event = new SyntheticCompositionEvent( + eventType, + domEventName, + null, + nativeEvent, + nativeEventTarget, + ); + dispatchQueue.push({event, listeners}); + if (fallbackData) { + // Inject data generated from fallback path into the synthetic event. + // This matches the property of native CompositionEventInterface. + event.data = fallbackData; + } else { + const customData = getDataFromCustomEvent(nativeEvent); + if (customData !== null) { + event.data = customData; + } } } } @@ -394,15 +396,18 @@ function extractBeforeInputEvent( return null; } - const event = new SyntheticInputEvent( - 'onBeforeInput', - 'beforeinput', - null, - nativeEvent, - nativeEventTarget, - ); - accumulateTwoPhaseListeners(targetInst, dispatchQueue, event); - event.data = chars; + const listeners = accumulateTwoPhaseListeners(targetInst, 'onBeforeInput'); + if (listeners.length > 0) { + const event = new SyntheticInputEvent( + 'onBeforeInput', + 'beforeinput', + null, + nativeEvent, + nativeEventTarget, + ); + dispatchQueue.push({event, listeners}); + event.data = chars; + } } /** diff --git a/packages/react-dom/src/events/plugins/ChangeEventPlugin.js b/packages/react-dom/src/events/plugins/ChangeEventPlugin.js index e9308c583a444..43d338a0cc5e2 100644 --- a/packages/react-dom/src/events/plugins/ChangeEventPlugin.js +++ b/packages/react-dom/src/events/plugins/ChangeEventPlugin.js @@ -49,16 +49,19 @@ function createAndAccumulateChangeEvent( nativeEvent, target, ) { - const event = new SyntheticEvent( - 'onChange', - 'change', - null, - nativeEvent, - target, - ); // Flag this event loop as needing state restore. enqueueStateRestore(((target: any): Node)); - accumulateTwoPhaseListeners(inst, dispatchQueue, event); + const listeners = accumulateTwoPhaseListeners(inst, 'onChange'); + if (listeners.length > 0) { + const event = new SyntheticEvent( + 'onChange', + 'change', + null, + nativeEvent, + target, + ); + dispatchQueue.push({event, listeners}); + } } /** * For IE shims diff --git a/packages/react-dom/src/events/plugins/SelectEventPlugin.js b/packages/react-dom/src/events/plugins/SelectEventPlugin.js index ff2c1a9d21f36..e1989b773b560 100644 --- a/packages/react-dom/src/events/plugins/SelectEventPlugin.js +++ b/packages/react-dom/src/events/plugins/SelectEventPlugin.js @@ -113,20 +113,21 @@ function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) { if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) { lastSelection = currentSelection; - const syntheticEvent = new SyntheticEvent( - 'onSelect', - 'select', - null, - nativeEvent, - nativeEventTarget, - ); - syntheticEvent.target = activeElement; - - accumulateTwoPhaseListeners( + const listeners = accumulateTwoPhaseListeners( activeElementInst, - dispatchQueue, - syntheticEvent, + 'onSelect', ); + if (listeners.length > 0) { + const event = new SyntheticEvent( + 'onSelect', + 'select', + null, + nativeEvent, + nativeEventTarget, + ); + dispatchQueue.push({event, listeners}); + event.target = activeElement; + } } } diff --git a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js index 2e4416c6a7c90..fafccff92014c 100644 --- a/packages/react-dom/src/events/plugins/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/plugins/SimpleEventPlugin.js @@ -63,7 +63,7 @@ function extractEvents( return; } let SyntheticEventCtor = SyntheticEvent; - let reactEventType = domEventName; + let reactEventType: string = domEventName; switch (domEventName) { case 'keypress': // Firefox creates a keypress event for function keys too. This removes @@ -157,25 +157,30 @@ function extractEvents( // Unknown event. This is used by createEventHandle. break; } - const event = new SyntheticEventCtor( - reactName, - reactEventType, - null, - nativeEvent, - nativeEventTarget, - ); const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0; if ( enableCreateEventHandleAPI && eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE ) { - accumulateEventHandleNonManagedNodeListeners( - dispatchQueue, - event, + const listeners = accumulateEventHandleNonManagedNodeListeners( + // TODO: this cast may not make sense for events like + // "focus" where React listens to e.g. "focusin". + ((reactEventType: any): DOMEventName), targetContainer, inCapturePhase, ); + if (listeners.length > 0) { + // Intentionally create event lazily. + const event = new SyntheticEventCtor( + reactName, + reactEventType, + null, + nativeEvent, + nativeEventTarget, + ); + dispatchQueue.push({event, listeners}); + } } else { // Some events don't bubble in the browser. // In the past, React has always bubbled them, but this can be surprising. @@ -189,13 +194,24 @@ function extractEvents( // This is a breaking change that can wait until React 18. domEventName === 'scroll'; - accumulateSinglePhaseListeners( + const listeners = accumulateSinglePhaseListeners( targetInst, - dispatchQueue, - event, + reactName, + nativeEvent.type, inCapturePhase, accumulateTargetOnly, ); + if (listeners.length > 0) { + // Intentionally create event lazily. + const event = new SyntheticEventCtor( + reactName, + reactEventType, + null, + nativeEvent, + nativeEventTarget, + ); + dispatchQueue.push({event, listeners}); + } } }