diff --git a/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js b/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js index 94a87f2c26b1..4c6b1309f5f2 100644 --- a/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js +++ b/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js @@ -261,5 +261,66 @@ if (isOSS) { root.destroy(); }, }, + ) + .test( + 'dispatch event, nested 50 deep (bubbling), stable tree', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); + root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(createNestedViews(50, ref)); + }); + }, + afterAll: () => { + root.destroy(); + }, + }, + ) + .test( + 'dispatch event, nested 50 deep (no handlers on ancestors), stable tree', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); + root = Fantom.createRoot(); + Fantom.runTask(() => { + let views: React.MixedElement = ( + {}} + style={{width: 10, height: 10}} + /> + ); + for (let i = 0; i < 50; i++) { + views = {views}; + } + root.render(views); + }); + }, + afterAll: () => { + root.destroy(); + }, + }, ); } diff --git a/packages/react-native/src/private/renderer/events/ReactNativeResponder.js b/packages/react-native/src/private/renderer/events/ReactNativeResponder.js index 63a9c6269e14..1323f08c4a0a 100644 --- a/packages/react-native/src/private/renderer/events/ReactNativeResponder.js +++ b/packages/react-native/src/private/renderer/events/ReactNativeResponder.js @@ -18,6 +18,7 @@ import { setCurrentTarget, setTarget, } from '../../webapis/dom/events/internals/EventInternals'; +import {getEventTargetParent} from '../../webapis/dom/events/internals/EventTargetInternals'; import { getCurrentProps, getNativeElementReference, @@ -78,6 +79,23 @@ function isEndish(topLevelType: string): boolean { return topLevelType === 'topTouchEnd' || topLevelType === 'topTouchCancel'; } +// Routes through the event-dispatch parent cache (shared with +// `EventTarget.getEventPath`) and stops the walk when the parent is not +// an element (e.g., when reaching the document at the top of the tree). +function getResponderParentElement( + node: ReadOnlyElement, +): ReadOnlyElement | null { + // `ReadOnlyElement` extends `EventTarget` at runtime when the new + // event-dispatching pipeline is enabled (the only case this module runs in). + // $FlowFixMe[incompatible-type] + const eventTarget: EventTarget = node; + const parent = getEventTargetParent(eventTarget); + if (parent instanceof ReadOnlyElement) { + return parent; + } + return null; +} + /** * Return the lowest common ancestor of A and B, or null if they are in * different trees. @@ -95,12 +113,12 @@ function getLowestCommonAncestor( } // Walk up from A until we find an ancestor that contains B - let current: ?ReadOnlyElement = instA.parentElement; + let current: ?ReadOnlyElement = getResponderParentElement(instA); while (current != null) { if (current.contains(instB)) { return current; } - current = current.parentElement; + current = getResponderParentElement(current); } return null; @@ -265,7 +283,7 @@ function negotiateResponder( } const dispatchNode: ReadOnlyElement | null = skipSelf - ? negotiationNode.parentElement + ? getResponderParentElement(negotiationNode) : negotiationNode; if (dispatchNode == null) { return null; @@ -276,7 +294,7 @@ function negotiateResponder( let node: ?ReadOnlyElement = dispatchNode; while (node != null) { path.unshift(node); - node = node.parentElement; + node = getResponderParentElement(node); } const dispatchConfig = responderEventTypes[shouldSetEventName]; diff --git a/packages/react-native/src/private/webapis/dom/events/EventTarget.js b/packages/react-native/src/private/webapis/dom/events/EventTarget.js index ab75d2ab9b63..92bebb55c4a5 100644 --- a/packages/react-native/src/private/webapis/dom/events/EventTarget.js +++ b/packages/react-native/src/private/webapis/dom/events/EventTarget.js @@ -34,6 +34,7 @@ import { EVENT_TARGET_GET_DECLARATIVE_LISTENER_KEY, EVENT_TARGET_GET_THE_PARENT_KEY, INTERNAL_DISPATCH_METHOD_KEY, + getEventTargetParent, } from './internals/EventTargetInternals'; export type EventCallback = (event: Event) => void; @@ -341,8 +342,7 @@ function getEventPath( while (target != null) { path.push(target); - // $FlowExpectedError[prop-missing] - target = target[EVENT_TARGET_GET_THE_PARENT_KEY](); + target = getEventTargetParent(target); } return path; diff --git a/packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js b/packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js index fa7bed2b7e69..93bf4803feaf 100644 --- a/packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js +++ b/packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js @@ -47,6 +47,27 @@ export const INTERNAL_DISPATCH_METHOD_KEY: symbol = Symbol( 'EventTarget[dispatch]', ); +const EVENT_DISPATCH_PARENT_CACHE_KEY: symbol = Symbol( + 'EventTarget[dispatch parent cache]', +); + +export function getEventTargetParent(target: EventTarget): EventTarget | null { + // The slot is `undefined` until populated; a populated slot may hold + // `null` (no parent), so check against `undefined` rather than nullishness. + // $FlowExpectedError[prop-missing] symbol-keyed slot + const cached: EventTarget | null | void = + // $FlowExpectedError[prop-missing] symbol-keyed slot + target[EVENT_DISPATCH_PARENT_CACHE_KEY]; + if (cached !== undefined) { + return cached; + } + // $FlowExpectedError[prop-missing] symbol-keyed method + const parent: EventTarget | null = target[EVENT_TARGET_GET_THE_PARENT_KEY](); + // $FlowExpectedError[prop-missing] symbol-keyed slot + target[EVENT_DISPATCH_PARENT_CACHE_KEY] = parent; + return parent; +} + /** * Dispatches a trusted event to the given event target. Mirrors the * `dispatchEvent` method on `EventTarget`: returns `false` if the event