Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Synthetic Events Lazily #19909

Merged
merged 1 commit into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 33 additions & 54 deletions packages/react-dom/src/events/DOMPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,31 +709,19 @@ function createDispatchListener(
};
}

function createDispatchEntry(
event: ReactSyntheticEvent,
listeners: Array<DispatchListener>,
): 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<DispatchListener> {
const captureName = reactName !== null ? reactName + 'Capture' : null;
const reactEventName = inCapturePhase ? captureName : reactName;
const listeners: Array<DispatchListener> = [];

let instance = targetFiber;
let lastHostComponent = null;
const targetType = event.nativeEvent.type;

// Accumulate all instances and listeners via the target -> root path.
while (instance !== null) {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not nullable now so we don't need extra checks below.

): Array<DispatchListener> {
const captureName = reactName + 'Capture';
const listeners: Array<DispatchListener> = [];
let instance = targetFiber;

Expand All @@ -833,29 +823,22 @@ export function accumulateTwoPhaseListeners(
// Handle listeners that are on HostComponents (i.e. <div>)
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 {
Expand Down Expand Up @@ -956,7 +939,7 @@ function accumulateEnterLeaveListenersForEvent(
instance = instance.return;
}
if (listeners.length !== 0) {
dispatchQueue.push(createDispatchEntry(event, listeners));
dispatchQueue.push({event, listeners});
}
}

Expand Down Expand Up @@ -995,27 +978,23 @@ export function accumulateEnterLeaveTwoPhaseListeners(
}

export function accumulateEventHandleNonManagedNodeListeners(
dispatchQueue: DispatchQueue,
event: ReactSyntheticEvent,
reactEventType: DOMEventName,
currentTarget: EventTarget,
inCapturePhase: boolean,
): void {
): Array<DispatchListener> {
const listeners: Array<DispatchListener> = [];

const eventListeners = getEventHandlerListeners(currentTarget);
if (eventListeners !== null) {
const targetType = ((event.type: any): DOMEventName);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if the discrepancy (the code path above reads event.nativeEvent.type instead) was intentional but for now I'm keeping it.

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(
Expand Down
57 changes: 31 additions & 26 deletions packages/react-dom/src/events/plugins/BeforeInputEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down
19 changes: 11 additions & 8 deletions packages/react-dom/src/events/plugins/ChangeEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 13 additions & 12 deletions packages/react-dom/src/events/plugins/SelectEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down
44 changes: 30 additions & 14 deletions packages/react-dom/src/events/plugins/SimpleEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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});
}
}
}

Expand Down