From 47883ab9115f7cad00abe35440300333010cc508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 7 Jan 2025 02:32:30 -0800 Subject: [PATCH] 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 | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 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 73d0a69adb0ded..001dde9ef09563 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) { @@ -139,7 +145,7 @@ export default class EventTarget { if (signal != null) { signal.addEventListener('abort', () => { - this.#removeEventListenerRegistration(listener, nonNullListenerList); + this._removeEventListenerRegistration(listener, nonNullListenerList); }); } } @@ -168,8 +174,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,7 +351,7 @@ export default class EventTarget { } } - #removeEventListenerRegistration( + _removeEventListenerRegistration( registration: EventListenerRegistration, listenerList: Array, ): void { @@ -359,6 +366,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. @@ -376,7 +401,7 @@ export default class EventTarget { */ // $FlowExpectedError[unsupported-syntax] [INTERNAL_DISPATCH_METHOD_KEY](event: Event): void { - this.#dispatch(event); + this._dispatch(event); } }