mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Optimize data structure to store listeners in EventTarget (#48964)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48964 Changelog: [internal] This replaces the data structure used to store listeners in `EventTarget`, from a map of arrays to a map of maps. This essentially optimizes listener registration/deregistraton at the expense of event dispatching. Given that it'll be common to have many nodes registering to events that are never dispatched, this might be the right trade-off. * Before: | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | --------------------------------------------------------------------- | --------------------- | ---------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'dispatchEvent, no bubbling, no listeners' | '4624.68 ± 0.49%' | '4570.00' | '218089 ± 0.02%' | '218818' | 216232 | | 1 | 'dispatchEvent, no bubbling, single listener' | '5771.34 ± 0.99%' | '5670.00' | '175389 ± 0.02%' | '176367' | 173270 | | 2 | 'dispatchEvent, no bubbling, multiple listeners' | '48207.35 ± 1.18%' | '47290.00' | '20964 ± 0.04%' | '21146' | 20744 | | 3 | 'dispatchEvent, bubbling, no listeners' | '185005.29 ± 0.16%' | '184060.00' | '5410 ± 0.05%' | '5433' | 5406 | | 4 | 'dispatchEvent, bubbling, single listener per target' | '286630.57 ± 0.11%' | '285560.00' | '3491 ± 0.06%' | '3502' | 3489 | | 5 | 'dispatchEvent, bubbling, multiple listeners per target' | '4435944.62 ± 0.27%' | '4425840.00 ± 30.00' | '226 ± 0.12%' | '226' | 1000 | | 6 | 'addEventListener, one listener' | '1734.88 ± 0.57%' | '1670.00' | '594938 ± 0.01%' | '598802' | 576411 | | 7 | 'addEventListener, one target, one type, multiple listeners' | '266031.11 ± 0.68%' | '261810.00' | '3781 ± 0.15%' | '3820' | 3759 | | 8 | 'addEventListener, one target, multiple types, one listener per type' | '124768.56 ± 0.39%' | '121160.00' | '8112 ± 0.16%' | '8254' | 8015 | | 9 | 'addEventListener, one target, multiple types, multiple listeners' | '27141326.31 ± 0.15%' | '27298945.00 ± 115.00' | '37 ± 0.15%' | '37' | 1000 | | 10 | 'addEventListener, multiple targets, one type, one listener' | '142646.24 ± 0.49%' | '137460.00' | '7123 ± 0.19%' | '7275' | 7011 | * After: | (index) | Task name | Latency average (ns) | Latency median (ns) | Throughput average (ops/s) | Throughput median (ops/s) | Samples | | ------- | --------------------------------------------------------------------- | --------------------- | --------------------- | -------------------------- | ------------------------- | ------- | | 0 | 'dispatchEvent, no bubbling, no listeners' | '4518.27 ± 0.51%' | '4460.00' | '223269 ± 0.02%' | '224215' | 221324 | | 1 | 'dispatchEvent, no bubbling, single listener' | '6563.10 ± 0.98%' | '6450.00' | '154311 ± 0.02%' | '155039' | 152367 | | 2 | 'dispatchEvent, no bubbling, multiple listeners' | '65429.69 ± 0.25%' | '64840.00' | '15330 ± 0.05%' | '15423' | 15284 | | 3 | 'dispatchEvent, bubbling, no listeners' | '181104.21 ± 0.13%' | '180300.00' | '5525 ± 0.05%' | '5546' | 5522 | | 4 | 'dispatchEvent, bubbling, single listener per target' | '367231.05 ± 0.10%' | '366035.00 ± 5.00' | '2724 ± 0.07%' | '2732' | 2724 | | 5 | 'dispatchEvent, bubbling, multiple listeners per target' | '6269141.58 ± 0.24%' | '6253275.00 ± 155.00' | '160 ± 0.11%' | '160' | 1000 | | 6 | 'addEventListener, one listener' | '1665.23 ± 0.51%' | '1610.00' | '618122 ± 0.01%' | '621118' | 600517 | | 7 | 'addEventListener, one target, one type, multiple listeners' | '97724.12 ± 1.34%' | '94640.00' | '10433 ± 0.14%' | '10566' | 10233 | | 8 | 'addEventListener, one target, multiple types, one listener per type' | '116915.60 ± 0.54%' | '113380.00' | '8707 ± 0.17%' | '8820' | 8554 | | 9 | 'addEventListener, one target, multiple types, multiple listeners' | '12276537.38 ± 0.42%' | '11924070.00 ± 10.00' | '82 ± 0.35%' | '84' | 1000 | | 10 | 'addEventListener, multiple targets, one type, one listener' | '133307.67 ± 0.58%' | '128340.00' | '7666 ± 0.20%' | '7792' | 7502 | Reviewed By: rshest Differential Revision: D68671944 fbshipit-source-id: 1a555ba9400e5830b4a56cdf12fd9418cd4a2a4f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
1f8ace8cce
commit
3d4906e7ec
@@ -57,7 +57,7 @@ type EventListenerRegistration = {
|
||||
removed: boolean,
|
||||
};
|
||||
|
||||
type ListenersMap = Map<string, Array<EventListenerRegistration>>;
|
||||
type ListenersMap = Map<string, Map<EventListener, EventListenerRegistration>>;
|
||||
|
||||
export default class EventTarget {
|
||||
addEventListener(
|
||||
@@ -112,21 +112,17 @@ export default class EventTarget {
|
||||
return;
|
||||
}
|
||||
|
||||
let listenersMap = getListenersMap(this, capture);
|
||||
let listenerList = listenersMap?.get(processedType);
|
||||
if (listenerList == null) {
|
||||
if (listenersMap == null) {
|
||||
listenersMap = new Map();
|
||||
setListenersMap(this, capture, listenersMap);
|
||||
}
|
||||
listenerList = [];
|
||||
listenersMap.set(processedType, listenerList);
|
||||
} else {
|
||||
for (const listener of listenerList) {
|
||||
if (listener.callback === callback) {
|
||||
return;
|
||||
}
|
||||
let listenersByType = getListenersForPhase(this, capture);
|
||||
let listeners = listenersByType?.get(processedType);
|
||||
if (listeners == null) {
|
||||
if (listenersByType == null) {
|
||||
listenersByType = new Map();
|
||||
setListenersMap(this, capture, listenersByType);
|
||||
}
|
||||
listeners = new Map();
|
||||
listenersByType.set(processedType, listeners);
|
||||
} else if (listeners.has(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listener: EventListenerRegistration = {
|
||||
@@ -135,15 +131,18 @@ export default class EventTarget {
|
||||
once,
|
||||
removed: false,
|
||||
};
|
||||
listenerList.push(listener);
|
||||
listeners.set(callback, listener);
|
||||
|
||||
const nonNullListenerList = listenerList;
|
||||
const nonNullListeners = listeners;
|
||||
|
||||
if (signal != null) {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
removeEventListenerRegistration(listener, nonNullListenerList);
|
||||
listener.removed = true;
|
||||
if (nonNullListeners.get(callback) === listener) {
|
||||
nonNullListeners.delete(callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
@@ -176,20 +175,16 @@ export default class EventTarget {
|
||||
? optionsOrUseCapture
|
||||
: Boolean(optionsOrUseCapture.capture);
|
||||
|
||||
const listenersMap = getListenersMap(this, capture);
|
||||
const listenerList = listenersMap?.get(processedType);
|
||||
if (listenerList == null) {
|
||||
const listenersByType = getListenersForPhase(this, capture);
|
||||
const listeners = listenersByType?.get(processedType);
|
||||
if (listeners == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < listenerList.length; i++) {
|
||||
const listener = listenerList[i];
|
||||
|
||||
if (listener.callback === callback) {
|
||||
listener.removed = true;
|
||||
listenerList.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
const listener = listeners.get(callback);
|
||||
if (listener != null) {
|
||||
listener.removed = true;
|
||||
listeners.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,22 +330,26 @@ function invoke(
|
||||
event: Event,
|
||||
eventPhase: EventPhase,
|
||||
) {
|
||||
const listenersMap = getListenersMap(
|
||||
const listenersByType = getListenersForPhase(
|
||||
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) {
|
||||
const maybeListeners = listenersByType?.get(event.type);
|
||||
if (maybeListeners == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a copy so listeners added during dispatch are NOT executed.
|
||||
// Note that `maybeListeners.values()` is a live view of the map instead of an
|
||||
// immutable copy.
|
||||
const listeners = Array.from(maybeListeners.values());
|
||||
|
||||
setCurrentTarget(event, eventTarget);
|
||||
|
||||
for (const listener of listenerList) {
|
||||
for (const listener of listeners) {
|
||||
if (listener.removed) {
|
||||
continue;
|
||||
}
|
||||
@@ -396,25 +395,10 @@ function invoke(
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
function getListenersForPhase(
|
||||
eventTarget: EventTarget,
|
||||
isCapture: boolean,
|
||||
): ?ListenersMap {
|
||||
|
||||
Reference in New Issue
Block a user