React Event System: Refactor ElementListenerMap for upgrading (#18308)
Dominic Gannaway committed
Mar 18, 2020 at 00:15 UTC
7d466bcc25876998d325dd506a81ab54846ddd11
7 files changed
+190
-36
packages/react-dom/src/client/ReactDOMComponent.js
+10
-5
@@ -1358,14 +1358,16 @@ export function listenToEventResponderEventTypes(
1358
// existing passive event listener before we add the
1359
// active event listener.
1360
const passiveKey = targetEventType + '_passive';
1361
- const passiveListener = listenerMap.get(passiveKey);
1362
- if (passiveListener != null) {
1361
+ const passiveItem = listenerMap.get(passiveKey);
1362
+ if (passiveItem !== undefined) {
1363
removeTrappedEventListener(
1364
document,
1365
- targetEventType,
1366
- passiveListener,
1365
+ (targetEventType: any),
1366
+ true,
1367
+ passiveItem.listener,
1368
true,
1369
);
1370
+ listenerMap.delete(passiveKey);
1371
}
1372
}
1373
const eventListener = addResponderEventSystemEvent(
@@ -1373,7 +1375,10 @@ export function listenToEventResponderEventTypes(
1375
targetEventType,
1376
isPassive,
1377
);
1376
- listenerMap.set(eventKey, eventListener);
1378
+ listenerMap.set(eventKey, {
1379
+ passive: isPassive,
1380
+ listener: eventListener,
1381
+ });
1382
}
1383
}
1384
}
packages/react-dom/src/events/DOMEventListenerMap.js
+7
-2
@@ -16,13 +16,18 @@ const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
16
const elementListenerMap:
17
// $FlowFixMe Work around Flow bug
18
| WeakMap
19
- | Map<EventTarget, Map<DOMTopLevelEventType | string, null | (any => void)>> = new PossiblyWeakMap();
19
+ | Map<EventTarget, ElementListenerMap> = new PossiblyWeakMap();
20
21
export type ElementListenerMap = Map<
22
DOMTopLevelEventType | string,
23
- null | (any => void),
23
+ ElementListenerMapEntry,
24
>;
25
26
+export type ElementListenerMapEntry = {
27
+ passive: void | boolean,
28
+ listener: any => void,
29
+};
30
+
31
export function getListenerMapForElement(
32
target: EventTarget,
33
): ElementListenerMap {
packages/react-dom/src/events/DOMLegacyEventPluginSystem.js
+21
-16
@@ -9,6 +9,7 @@
9
10
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
11
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
12
+import type {ElementListenerMap} from '../events/DOMEventListenerMap';
13
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
14
import type {Fiber} from 'react-reconciler/src/ReactFiber';
15
import type {PluginModule} from 'legacy-events/PluginModuleType';
@@ -323,57 +324,61 @@ export function legacyListenToEvent(
324
export function legacyListenToTopLevelEvent(
325
topLevelType: DOMTopLevelEventType,
326
mountAt: Document | Element,
326
- listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>,
327
+ listenerMap: ElementListenerMap,
328
): void {
329
if (!listenerMap.has(topLevelType)) {
330
switch (topLevelType) {
330
- case TOP_SCROLL:
331
- legacyTrapCapturedEvent(TOP_SCROLL, mountAt);
331
+ case TOP_SCROLL: {
332
+ legacyTrapCapturedEvent(TOP_SCROLL, mountAt, listenerMap);
333
break;
334
+ }
335
case TOP_FOCUS:
336
case TOP_BLUR:
335
- legacyTrapCapturedEvent(TOP_FOCUS, mountAt);
336
- legacyTrapCapturedEvent(TOP_BLUR, mountAt);
337
- // We set the flag for a single dependency later in this function,
338
- // but this ensures we mark both as attached rather than just one.
339
- listenerMap.set(TOP_BLUR, null);
340
- listenerMap.set(TOP_FOCUS, null);
337
+ legacyTrapCapturedEvent(TOP_FOCUS, mountAt, listenerMap);
338
+ legacyTrapCapturedEvent(TOP_BLUR, mountAt, listenerMap);
339
break;
340
case TOP_CANCEL:
343
- case TOP_CLOSE:
341
+ case TOP_CLOSE: {
342
if (isEventSupported(getRawEventName(topLevelType))) {
345
- legacyTrapCapturedEvent(topLevelType, mountAt);
343
+ legacyTrapCapturedEvent(topLevelType, mountAt, listenerMap);
344
}
345
break;
346
+ }
347
case TOP_INVALID:
348
case TOP_SUBMIT:
349
case TOP_RESET:
350
// We listen to them on the target DOM elements.
351
// Some of them bubble so we don't want them to fire twice.
352
break;
354
- default:
353
+ default: {
354
// By default, listen on the top level to all non-media events.
355
// Media events don't bubble so adding the listener wouldn't do anything.
356
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
357
if (!isMediaEvent) {
359
- legacyTrapBubbledEvent(topLevelType, mountAt);
358
+ legacyTrapBubbledEvent(topLevelType, mountAt, listenerMap);
359
}
360
break;
361
+ }
362
}
363
- listenerMap.set(topLevelType, null);
363
}
364
}
365
366
export function legacyTrapBubbledEvent(
367
topLevelType: DOMTopLevelEventType,
368
element: Document | Element,
369
+ listenerMap?: ElementListenerMap,
370
): void {
371
- addTrappedEventListener(element, topLevelType, false);
371
+ const listener = addTrappedEventListener(element, topLevelType, false);
372
+ if (listenerMap) {
373
+ listenerMap.set(topLevelType, {passive: undefined, listener});
374
+ }
375
}
376
377
export function legacyTrapCapturedEvent(
378
topLevelType: DOMTopLevelEventType,
379
element: Document | Element,
380
+ listenerMap: ElementListenerMap,
381
): void {
378
- addTrappedEventListener(element, topLevelType, true);
382
+ const listener = addTrappedEventListener(element, topLevelType, true);
383
+ listenerMap.set(topLevelType, {passive: undefined, listener});
384
}
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+42
-7
@@ -9,7 +9,10 @@
9
10
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
11
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
12
-import type {ElementListenerMap} from '../events/DOMEventListenerMap';
12
+import type {
13
+ ElementListenerMap,
14
+ ElementListenerMapEntry,
15
+} from '../events/DOMEventListenerMap';
16
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
17
import type {EventPriority} from 'shared/ReactTypes';
18
import type {Fiber} from 'react-reconciler/src/ReactFiber';
@@ -24,7 +27,10 @@ import {plugins} from 'legacy-events/EventPluginRegistry';
27
28
import {HostRoot, HostPortal} from 'shared/ReactWorkTags';
29
27
-import {addTrappedEventListener} from './ReactDOMEventListener';
30
+import {
31
+ addTrappedEventListener,
32
+ removeTrappedEventListener,
33
+} from './ReactDOMEventListener';
34
import getEventTarget from './getEventTarget';
35
import {getListenerMapForElement} from './DOMEventListenerMap';
36
import {
@@ -153,6 +159,25 @@ function dispatchEventsForPlugins(
159
}
160
}
161
162
+function shouldUpgradeListener(
163
+ listenerEntry: void | ElementListenerMapEntry,
164
+ passive: void | boolean,
165
+): boolean {
166
+ if (listenerEntry === undefined) {
167
+ return false;
168
+ }
169
+ // Upgrade from passive to active.
170
+ if (passive !== true && listenerEntry.passive) {
171
+ return true;
172
+ }
173
+ // Upgrade from default-active (browser default) to active.
174
+ if (passive === false && listenerEntry.passive === undefined) {
175
+ return true;
176
+ }
177
+ // Otherwise, do not upgrade
178
+ return false;
179
+}
180
+
181
export function listenToTopLevelEvent(
182
topLevelType: DOMTopLevelEventType,
183
targetContainer: EventTarget,
@@ -160,11 +185,21 @@ export function listenToTopLevelEvent(
185
passive?: boolean,
186
priority?: EventPriority,
187
): void {
163
- // TODO: we need to know if the listenerMap previously was passive
164
- // and to check if we need to upgrade to active. This will come in
165
- // a useEvent follow up PR.
166
- if (!listenerMap.has(topLevelType)) {
188
+ const listenerEntry = listenerMap.get(topLevelType);
189
+ const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive);
190
+ if (listenerEntry === undefined || shouldUpgrade) {
191
const isCapturePhase = capturePhaseEvents.has(topLevelType);
192
+ // If we should upgrade, then we need to remove the existing trapped
193
+ // event listener for the target container.
194
+ if (shouldUpgrade) {
195
+ removeTrappedEventListener(
196
+ targetContainer,
197
+ topLevelType,
198
+ isCapturePhase,
199
+ ((listenerEntry: any): ElementListenerMapEntry).listener,
200
+ ((listenerEntry: any): ElementListenerMapEntry).passive,
201
+ );
202
+ }
203
const listener = addTrappedEventListener(
204
targetContainer,
205
topLevelType,
@@ -173,7 +208,7 @@ export function listenToTopLevelEvent(
208
passive,
209
priority,
210
);
176
- listenerMap.set(topLevelType, listener);
211
+ listenerMap.set(topLevelType, {passive, listener});
212
}
213
}
214
packages/react-dom/src/events/ReactDOMEventListener.js
+7
-5
@@ -255,20 +255,22 @@ export function addTrappedEventListener(
255
256
export function removeTrappedEventListener(
257
targetContainer: EventTarget,
258
- topLevelType: string,
258
+ topLevelType: DOMTopLevelEventType,
259
+ capture: boolean,
260
listener: any => void,
260
- passive: boolean,
261
+ passive: void | boolean,
262
) {
263
if (listener.remove != null) {
264
listener.remove();
265
} else {
266
+ const rawEventName = getRawEventName(topLevelType);
267
if (passiveBrowserEventsSupported) {
266
- targetContainer.removeEventListener(topLevelType, listener, {
267
- capture: true,
268
+ targetContainer.removeEventListener(rawEventName, listener, {
269
+ capture,
270
passive,
271
});
272
} else {
271
- targetContainer.removeEventListener(topLevelType, listener, true);
273
+ targetContainer.removeEventListener(rawEventName, listener, capture);
274
}
275
}
276
}
packages/react-dom/src/events/ReactDOMEventReplaying.js
+1
-1
@@ -244,7 +244,7 @@ function trapReplayableEventForDocument(
244
topLevelTypeString,
245
false,
246
);
247
- listenerMap.set(activeEventKey, listener);
247
+ listenerMap.set(activeEventKey, {passive: false, listener});
248
}
249
}
250
}
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+102
@@ -1648,5 +1648,107 @@ describe('DOMModernPluginEventSystem', () => {
1648
dispatchClickEvent(ref.current);
1649
expect(log).toEqual([{counter: 1}]);
1650
});
1651
+
1652
+ it('should correctly work for a basic "click" listener that upgrades', () => {
1653
+ const clickEvent = jest.fn();
1654
+ const buttonRef = React.createRef();
1655
+ const button2Ref = React.createRef();
1656
+
1657
+ function Test2() {
1658
+ const click = ReactDOM.unstable_useEvent('click', {
1659
+ passive: false,
1660
+ });
1661
+
1662
+ React.useEffect(() => {
1663
+ click.setListener(button2Ref.current, clickEvent);
1664
+ });
1665
+
1666
+ return <button ref={button2Ref}>Click me!</button>;
1667
+ }
1668
+
1669
+ function Test({extra}) {
1670
+ const click = ReactDOM.unstable_useEvent('click', {
1671
+ passive: true,
1672
+ });
1673
+
1674
+ React.useEffect(() => {
1675
+ click.setListener(buttonRef.current, clickEvent);
1676
+ });
1677
+
1678
+ return (
1679
+ <>
1680
+ <button ref={buttonRef}>Click me!</button>
1681
+ {extra && <Test2 />}
1682
+ </>
1683
+ );
1684
+ }
1685
+
1686
+ ReactDOM.render(<Test />, container);
1687
+ Scheduler.unstable_flushAll();
1688
+
1689
+ let button = buttonRef.current;
1690
+ dispatchClickEvent(button);
1691
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1692
+
1693
+ ReactDOM.render(<Test extra={true} />, container);
1694
+ Scheduler.unstable_flushAll();
1695
+
1696
+ clickEvent.mockClear();
1697
+
1698
+ button = button2Ref.current;
1699
+ dispatchClickEvent(button);
1700
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1701
+ });
1702
+
1703
+ it('should correctly work for a basic "click" listener that upgrades #2', () => {
1704
+ const clickEvent = jest.fn();
1705
+ const buttonRef = React.createRef();
1706
+ const button2Ref = React.createRef();
1707
+
1708
+ function Test2() {
1709
+ const click = ReactDOM.unstable_useEvent('click', {
1710
+ passive: false,
1711
+ });
1712
+
1713
+ React.useEffect(() => {
1714
+ click.setListener(button2Ref.current, clickEvent);
1715
+ });
1716
+
1717
+ return <button ref={button2Ref}>Click me!</button>;
1718
+ }
1719
+
1720
+ function Test({extra}) {
1721
+ const click = ReactDOM.unstable_useEvent('click', {
1722
+ passive: undefined,
1723
+ });
1724
+
1725
+ React.useEffect(() => {
1726
+ click.setListener(buttonRef.current, clickEvent);
1727
+ });
1728
+
1729
+ return (
1730
+ <>
1731
+ <button ref={buttonRef}>Click me!</button>
1732
+ {extra && <Test2 />}
1733
+ </>
1734
+ );
1735
+ }
1736
+
1737
+ ReactDOM.render(<Test />, container);
1738
+ Scheduler.unstable_flushAll();
1739
+
1740
+ let button = buttonRef.current;
1741
+ dispatchClickEvent(button);
1742
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1743
+
1744
+ ReactDOM.render(<Test extra={true} />, container);
1745
+ Scheduler.unstable_flushAll();
1746
+
1747
+ clickEvent.mockClear();
1748
+
1749
+ button = button2Ref.current;
1750
+ dispatchClickEvent(button);
1751
+ expect(clickEvent).toHaveBeenCalledTimes(1);
1752
+ });
1753
});
1754
});