From 554d6705f9cd8beafe9112661121e322886968ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Sat, 4 Jan 2025 11:14:10 -0800 Subject: [PATCH] [skip ci] Make EventTarget compatible with the existing implementation of ReadOnlyNode (#48427) Summary: Changelog: [internal] The `ReactNativeElement` class was refactored for performance reasons, and the current implementation does **NOT** call `super()`, and it inlines the parent constructor instead. When it eventually extends `EventTarget`, things won't work as expected because the existing `EventTarget` implementation has constructor dependencies. This refactors the current implementation of `EventTarget` to eliminate those constructor side-effects, and eliminates the constructor altogether. This breaks encapsulation, but it has some positive side-effects on performance: 1. Creating `EventTarget` instances is faster because it has no constructor logic. 2. Improves memory by not creating maps to hold the event listeners if no event listeners are ever added to the target (which is very common). 3. Improves the overall runtime performance of the methods in the class by migrating away from private methods (which are known to be slow on the babel transpiled version we're currently using). Extra: it also simplifies making window/the global scope implement the EventTarget interface :) Differential Revision: D67758408 --- .../private/webapis/dom/events/EventTarget.js | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) 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 8d4e8bc9bd3e67..c69163a4bd014e 100644 --- a/packages/react-native/src/private/webapis/dom/events/EventTarget.js +++ b/packages/react-native/src/private/webapis/dom/events/EventTarget.js @@ -57,6 +57,11 @@ type EventListenerRegistration = { removed: boolean, }; +type ListenersMap = Map>; + +const CAPTURING_LISTENERS_KEY = Symbol('capturingListeners'); +const BUBBLING_LISTENERS_KEY = Symbol('bubblingListeners'); + function getDefaultPassiveValue( type: string, eventTarget: EventTarget, @@ -65,9 +70,6 @@ function getDefaultPassiveValue( } export default class EventTarget { - #listeners: Map> = new Map(); - #captureListeners: Map> = new Map(); - addEventListener( type: string, callback: EventListener | null, @@ -114,11 +116,15 @@ export default class EventTarget { return; } - const listenerMap = capture ? this.#captureListeners : this.#listeners; - let listenerList = listenerMap.get(processedType); + let listenersMap = this._getListenersMap(capture); + let listenerList = listenersMap?.get(processedType); if (listenerList == null) { + if (listenersMap == null) { + listenersMap = new Map(); + this._setListenersMap(capture, listenersMap); + } listenerList = []; - listenerMap.set(processedType, listenerList); + listenersMap.set(processedType, listenerList); } else { for (const listener of listenerList) { if (listener.callback === callback) { @@ -165,8 +171,8 @@ export default class EventTarget { ? optionsOrUseCapture : optionsOrUseCapture.capture ?? false; - const listenerMap = capture ? this.#captureListeners : this.#listeners; - const listenerList = listenerMap.get(processedType); + const listenersMap = this._getListenersMap(capture); + const listenerList = listenersMap?.get(processedType); if (listenerList == null) { return; } @@ -200,7 +206,7 @@ export default class EventTarget { setIsTrusted(event, false); - this.#dispatch(event); + this._dispatch(event); return !event.defaultPrevented; } @@ -213,10 +219,10 @@ export default class EventTarget { * Implements the "event dispatch" concept * (see https://dom.spec.whatwg.org/#concept-event-dispatch). */ - #dispatch(event: Event): void { + _dispatch(event: Event): void { setEventDispatchFlag(event, true); - const eventPath = this.#getEventPath(event); + const eventPath = this._getEventPath(event); setComposedPath(event, eventPath); setTarget(event, this); @@ -230,7 +236,7 @@ export default class EventTarget { event, target === this ? Event.AT_TARGET : Event.CAPTURING_PHASE, ); - target.#invoke(event, Event.CAPTURING_PHASE); + target._invoke(event, Event.CAPTURING_PHASE); } for (const target of eventPath) { @@ -248,7 +254,7 @@ export default class EventTarget { event, target === this ? Event.AT_TARGET : Event.BUBBLING_PHASE, ); - target.#invoke(event, Event.BUBBLING_PHASE); + target._invoke(event, Event.BUBBLING_PHASE); } setEventPhase(event, Event.NONE); @@ -266,7 +272,7 @@ export default class EventTarget { * * The return value is also set as `composedPath` for the event. */ - #getEventPath(event: Event): $ReadOnlyArray { + _getEventPath(event: Event): $ReadOnlyArray { const path = []; // eslint-disable-next-line consistent-this let target: EventTarget | null = this; @@ -284,20 +290,21 @@ export default class EventTarget { * Implements the event listener invoke concept * (see https://dom.spec.whatwg.org/#concept-event-listener-invoke). */ - #invoke(event: Event, eventPhase: EventPhase) { - const listenerMap = - eventPhase === Event.CAPTURING_PHASE - ? this.#captureListeners - : this.#listeners; + _invoke(event: Event, eventPhase: EventPhase) { + const listenersMap = this._getListenersMap( + eventPhase === Event.CAPTURING_PHASE, + ); setCurrentTarget(event, this); // This is a copy so listeners added during dispatch are NOT executed. - const listenerList = listenerMap.get(event.type)?.slice(); + const listenerList = listenersMap?.get(event.type)?.slice(); if (listenerList == null) { return; } + setCurrentTarget(event, this); + for (const listener of listenerList) { if (listener.removed) { continue; @@ -344,6 +351,24 @@ export default class EventTarget { } } + _getListenersMap(isCapture: boolean): ?ListenersMap { + return isCapture + ? // $FlowExpectedError[prop-missing] + this[CAPTURING_LISTENERS_KEY] + : // $FlowExpectedError[prop-missing] + this[BUBBLING_LISTENERS_KEY]; + } + + _setListenersMap(isCapture: boolean, listenersMap: ListenersMap): void { + if (isCapture) { + // $FlowExpectedError[prop-missing] + this[CAPTURING_LISTENERS_KEY] = listenersMap; + } else { + // $FlowExpectedError[prop-missing] + this[BUBBLING_LISTENERS_KEY] = listenersMap; + } + } + /** * This a "protected" method to be overridden by a subclass to allow event * propagation. @@ -361,7 +386,7 @@ export default class EventTarget { */ // $FlowExpectedError[unsupported-syntax] [INTERNAL_DISPATCH_METHOD_KEY](event: Event): void { - this.#dispatch(event); + this._dispatch(event); } }