@samitouri / QOS-React-2 / commits / 7d466bcc25

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 });