mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Move all properties of EventTarget to symbols to avoid polluting the global scope (#48962)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48962 Changelog: [internal] We moved away from private fields for `EventTarget` for performance, in favor of normal fields prefixed with underscore, but this would pollute the global scope when we make it extend `EventTarget`. This refactors the implementation to use symbols for all properties to avoid that problem, leaving only `addEventListener` and `removeEventListener` as regular properties in the prototype. Performance-wise this is neutral or a slight improvement according to the existing benchmark. Reviewed By: javache Differential Revision: D68672215 fbshipit-source-id: b329548efce6059ae2b9f33afa0719e057d3b8ba
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b28ab79f1b
commit
9fbc02a031
+203
-189
@@ -59,16 +59,6 @@ type EventListenerRegistration = {
|
||||
|
||||
type ListenersMap = Map<string, Array<EventListenerRegistration>>;
|
||||
|
||||
const CAPTURING_LISTENERS_KEY = Symbol('capturingListeners');
|
||||
const BUBBLING_LISTENERS_KEY = Symbol('bubblingListeners');
|
||||
|
||||
function getDefaultPassiveValue(
|
||||
type: string,
|
||||
eventTarget: EventTarget,
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class EventTarget {
|
||||
addEventListener(
|
||||
type: string,
|
||||
@@ -122,12 +112,12 @@ export default class EventTarget {
|
||||
return;
|
||||
}
|
||||
|
||||
let listenersMap = this._getListenersMap(capture);
|
||||
let listenersMap = getListenersMap(this, capture);
|
||||
let listenerList = listenersMap?.get(processedType);
|
||||
if (listenerList == null) {
|
||||
if (listenersMap == null) {
|
||||
listenersMap = new Map();
|
||||
this._setListenersMap(capture, listenersMap);
|
||||
setListenersMap(this, capture, listenersMap);
|
||||
}
|
||||
listenerList = [];
|
||||
listenersMap.set(processedType, listenerList);
|
||||
@@ -153,7 +143,7 @@ export default class EventTarget {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
this._removeEventListenerRegistration(listener, nonNullListenerList);
|
||||
removeEventListenerRegistration(listener, nonNullListenerList);
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
@@ -186,7 +176,7 @@ export default class EventTarget {
|
||||
? optionsOrUseCapture
|
||||
: Boolean(optionsOrUseCapture.capture);
|
||||
|
||||
const listenersMap = this._getListenersMap(capture);
|
||||
const listenersMap = getListenersMap(this, capture);
|
||||
const listenerList = listenersMap?.get(processedType);
|
||||
if (listenerList == null) {
|
||||
return;
|
||||
@@ -218,184 +208,11 @@ export default class EventTarget {
|
||||
|
||||
setIsTrusted(event, false);
|
||||
|
||||
this._dispatch(event);
|
||||
dispatch(this, event);
|
||||
|
||||
return !event.defaultPrevented;
|
||||
}
|
||||
|
||||
/**
|
||||
* This internal version of `dispatchEvent` does not validate the input and
|
||||
* does not reset the `isTrusted` flag, so it can be used for both trusted
|
||||
* and not trusted events.
|
||||
*
|
||||
* Implements the "event dispatch" concept
|
||||
* (see https://dom.spec.whatwg.org/#concept-event-dispatch).
|
||||
*/
|
||||
_dispatch(event: Event): void {
|
||||
setEventDispatchFlag(event, true);
|
||||
|
||||
const eventPath = this._getEventPath(event);
|
||||
setComposedPath(event, eventPath);
|
||||
setTarget(event, this);
|
||||
|
||||
for (let i = eventPath.length - 1; i >= 0; i--) {
|
||||
if (getStopPropagationFlag(event)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const target = eventPath[i];
|
||||
setEventPhase(
|
||||
event,
|
||||
target === this ? Event.AT_TARGET : Event.CAPTURING_PHASE,
|
||||
);
|
||||
target._invoke(event, Event.CAPTURING_PHASE);
|
||||
}
|
||||
|
||||
for (const target of eventPath) {
|
||||
if (getStopPropagationFlag(event)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the event does NOT bubble, we only dispatch the event to the
|
||||
// target in the bubbling phase.
|
||||
if (!event.bubbles && target !== this) {
|
||||
break;
|
||||
}
|
||||
|
||||
setEventPhase(
|
||||
event,
|
||||
target === this ? Event.AT_TARGET : Event.BUBBLING_PHASE,
|
||||
);
|
||||
target._invoke(event, Event.BUBBLING_PHASE);
|
||||
}
|
||||
|
||||
setEventPhase(event, Event.NONE);
|
||||
setCurrentTarget(event, null);
|
||||
setComposedPath(event, []);
|
||||
|
||||
setEventDispatchFlag(event, false);
|
||||
setStopImmediatePropagationFlag(event, false);
|
||||
setStopPropagationFlag(event, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the event path for an event about to be dispatched in this target
|
||||
* (see https://dom.spec.whatwg.org/#event-path).
|
||||
*
|
||||
* The return value is also set as `composedPath` for the event.
|
||||
*/
|
||||
_getEventPath(event: Event): $ReadOnlyArray<EventTarget> {
|
||||
const path = [];
|
||||
// eslint-disable-next-line consistent-this
|
||||
let target: EventTarget | null = this;
|
||||
|
||||
while (target != null) {
|
||||
path.push(target);
|
||||
// $FlowExpectedError[prop-missing]
|
||||
target = target[EVENT_TARGET_GET_THE_PARENT_KEY]();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the event listener invoke concept
|
||||
* (see https://dom.spec.whatwg.org/#concept-event-listener-invoke).
|
||||
*/
|
||||
_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 = listenersMap?.get(event.type)?.slice();
|
||||
if (listenerList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentTarget(event, this);
|
||||
|
||||
for (const listener of listenerList) {
|
||||
if (listener.removed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (listener.once) {
|
||||
this.removeEventListener(
|
||||
event.type,
|
||||
listener.callback,
|
||||
eventPhase === Event.CAPTURING_PHASE,
|
||||
);
|
||||
}
|
||||
|
||||
if (listener.passive) {
|
||||
setInPassiveListenerFlag(event, true);
|
||||
}
|
||||
|
||||
const currentEvent = global.event;
|
||||
global.event = event;
|
||||
|
||||
const callback = listener.callback;
|
||||
|
||||
try {
|
||||
if (typeof callback === 'function') {
|
||||
callback.call(this, event);
|
||||
// $FlowExpectedError[method-unbinding]
|
||||
} else if (typeof callback.handleEvent === 'function') {
|
||||
callback.handleEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: replace with `reportError` when it's available.
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (listener.passive) {
|
||||
setInPassiveListenerFlag(event, false);
|
||||
}
|
||||
|
||||
global.event = currentEvent;
|
||||
|
||||
if (getStopImmediatePropagationFlag(event)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_removeEventListenerRegistration(
|
||||
registration: EventListenerRegistration,
|
||||
listenerList: Array<EventListenerRegistration>,
|
||||
): void {
|
||||
for (let i = 0; i < listenerList.length; i++) {
|
||||
const listener = listenerList[i];
|
||||
|
||||
if (listener === registration) {
|
||||
listener.removed = true;
|
||||
listenerList.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_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.
|
||||
@@ -413,7 +230,7 @@ export default class EventTarget {
|
||||
*/
|
||||
// $FlowExpectedError[unsupported-syntax]
|
||||
[INTERNAL_DISPATCH_METHOD_KEY](event: Event): void {
|
||||
this._dispatch(event);
|
||||
dispatch(this, event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,6 +242,203 @@ function validateCallback(callback: EventListener, methodName: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultPassiveValue(
|
||||
type: string,
|
||||
eventTarget: EventTarget,
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This internal version of `dispatchEvent` does not validate the input and
|
||||
* does not reset the `isTrusted` flag, so it can be used for both trusted
|
||||
* and not trusted events.
|
||||
*
|
||||
* Implements the "event dispatch" concept
|
||||
* (see https://dom.spec.whatwg.org/#concept-event-dispatch).
|
||||
*/
|
||||
function dispatch(eventTarget: EventTarget, event: Event): void {
|
||||
setEventDispatchFlag(event, true);
|
||||
|
||||
const eventPath = getEventPath(eventTarget, event);
|
||||
setComposedPath(event, eventPath);
|
||||
setTarget(event, eventTarget);
|
||||
|
||||
for (let i = eventPath.length - 1; i >= 0; i--) {
|
||||
if (getStopPropagationFlag(event)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const target = eventPath[i];
|
||||
setEventPhase(
|
||||
event,
|
||||
target === eventTarget ? Event.AT_TARGET : Event.CAPTURING_PHASE,
|
||||
);
|
||||
invoke(target, event, Event.CAPTURING_PHASE);
|
||||
}
|
||||
|
||||
for (const target of eventPath) {
|
||||
if (getStopPropagationFlag(event)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the event does NOT bubble, we only dispatch the event to the
|
||||
// target in the bubbling phase.
|
||||
if (!event.bubbles && target !== eventTarget) {
|
||||
break;
|
||||
}
|
||||
|
||||
setEventPhase(
|
||||
event,
|
||||
target === eventTarget ? Event.AT_TARGET : Event.BUBBLING_PHASE,
|
||||
);
|
||||
invoke(target, event, Event.BUBBLING_PHASE);
|
||||
}
|
||||
|
||||
setEventPhase(event, Event.NONE);
|
||||
setCurrentTarget(event, null);
|
||||
setComposedPath(event, []);
|
||||
|
||||
setEventDispatchFlag(event, false);
|
||||
setStopImmediatePropagationFlag(event, false);
|
||||
setStopPropagationFlag(event, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the event path for an event about to be dispatched in this target
|
||||
* (see https://dom.spec.whatwg.org/#event-path).
|
||||
*
|
||||
* The return value is also set as `composedPath` for the event.
|
||||
*/
|
||||
function getEventPath(
|
||||
eventTarget: EventTarget,
|
||||
event: Event,
|
||||
): $ReadOnlyArray<EventTarget> {
|
||||
const path = [];
|
||||
let target: EventTarget | null = eventTarget;
|
||||
|
||||
while (target != null) {
|
||||
path.push(target);
|
||||
// $FlowExpectedError[prop-missing]
|
||||
target = target[EVENT_TARGET_GET_THE_PARENT_KEY]();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the event listener invoke concept
|
||||
* (see https://dom.spec.whatwg.org/#concept-event-listener-invoke).
|
||||
*/
|
||||
function invoke(
|
||||
eventTarget: EventTarget,
|
||||
event: Event,
|
||||
eventPhase: EventPhase,
|
||||
) {
|
||||
const listenersMap = getListenersMap(
|
||||
eventTarget,
|
||||
eventPhase === Event.CAPTURING_PHASE,
|
||||
);
|
||||
|
||||
setCurrentTarget(event, eventTarget);
|
||||
|
||||
// This is a copy so listeners added during dispatch are NOT executed.
|
||||
const listenerList = listenersMap?.get(event.type)?.slice();
|
||||
if (listenerList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentTarget(event, eventTarget);
|
||||
|
||||
for (const listener of listenerList) {
|
||||
if (listener.removed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (listener.once) {
|
||||
eventTarget.removeEventListener(
|
||||
event.type,
|
||||
listener.callback,
|
||||
eventPhase === Event.CAPTURING_PHASE,
|
||||
);
|
||||
}
|
||||
|
||||
if (listener.passive) {
|
||||
setInPassiveListenerFlag(event, true);
|
||||
}
|
||||
|
||||
const currentEvent = global.event;
|
||||
global.event = event;
|
||||
|
||||
const callback = listener.callback;
|
||||
|
||||
try {
|
||||
if (typeof callback === 'function') {
|
||||
callback.call(eventTarget, event);
|
||||
// $FlowExpectedError[method-unbinding]
|
||||
} else if (typeof callback.handleEvent === 'function') {
|
||||
callback.handleEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: replace with `reportError` when it's available.
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (listener.passive) {
|
||||
setInPassiveListenerFlag(event, false);
|
||||
}
|
||||
|
||||
global.event = currentEvent;
|
||||
|
||||
if (getStopImmediatePropagationFlag(event)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeEventListenerRegistration(
|
||||
registration: EventListenerRegistration,
|
||||
listenerList: Array<EventListenerRegistration>,
|
||||
): void {
|
||||
for (let i = 0; i < listenerList.length; i++) {
|
||||
const listener = listenerList[i];
|
||||
|
||||
if (listener === registration) {
|
||||
listener.removed = true;
|
||||
listenerList.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CAPTURING_LISTENERS_KEY = Symbol('capturingListeners');
|
||||
const BUBBLING_LISTENERS_KEY = Symbol('bubblingListeners');
|
||||
|
||||
function getListenersMap(
|
||||
eventTarget: EventTarget,
|
||||
isCapture: boolean,
|
||||
): ?ListenersMap {
|
||||
return isCapture
|
||||
? // $FlowExpectedError[prop-missing]
|
||||
eventTarget[CAPTURING_LISTENERS_KEY]
|
||||
: // $FlowExpectedError[prop-missing]
|
||||
eventTarget[BUBBLING_LISTENERS_KEY];
|
||||
}
|
||||
|
||||
function setListenersMap(
|
||||
eventTarget: EventTarget,
|
||||
isCapture: boolean,
|
||||
listenersMap: ListenersMap,
|
||||
): void {
|
||||
if (isCapture) {
|
||||
// $FlowExpectedError[prop-missing]
|
||||
eventTarget[CAPTURING_LISTENERS_KEY] = listenersMap;
|
||||
} else {
|
||||
// $FlowExpectedError[prop-missing]
|
||||
eventTarget[BUBBLING_LISTENERS_KEY] = listenersMap;
|
||||
}
|
||||
}
|
||||
|
||||
const EVENT_DISPATCH_FLAG = Symbol('Event.dispatch');
|
||||
|
||||
function getEventDispatchFlag(event: Event): boolean {
|
||||
|
||||
Reference in New Issue
Block a user