From dce430ad92d7ed6be5b934f4263d4a39e068ee29 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 19 Aug 2019 19:22:46 +0100 Subject: [PATCH] [Flare] Rework the responder dispatching/batching mechanism (#16334) --- .../legacy-events/ReactGenericBatching.js | 20 +- .../src/events/DOMEventResponderSystem.js | 253 +++++------------- .../DOMEventResponderSystem-test.internal.js | 33 --- packages/react-events/README.md | 19 -- packages/react-events/src/dom/Drag.js | 27 +- packages/react-events/src/dom/Focus.js | 14 - packages/react-events/src/dom/Hover.js | 2 - packages/react-events/src/dom/Press.js | 7 - packages/react-events/src/dom/Scroll.js | 7 - packages/react-events/src/dom/Swipe.js | 31 +-- .../src/dom/__tests__/Drag-test.internal.js | 55 ---- .../src/ReactFabricEventResponderSystem.js | 238 ++++------------ packages/shared/ReactDOMTypes.js | 3 - packages/shared/ReactTypes.js | 3 - packages/shared/createEventResponder.js | 2 - 15 files changed, 149 insertions(+), 565 deletions(-) diff --git a/packages/legacy-events/ReactGenericBatching.js b/packages/legacy-events/ReactGenericBatching.js index e92f15b7af95d..d3cf009c4571a 100644 --- a/packages/legacy-events/ReactGenericBatching.js +++ b/packages/legacy-events/ReactGenericBatching.js @@ -11,6 +11,8 @@ import { } from './ReactControlledComponent'; import {enableFlareAPI} from 'shared/ReactFeatureFlags'; +import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; + // Used as a way to call batchedUpdates when we don't have a reference to // the renderer. Such as when we're dispatching events or if third party // libraries need to call batchedUpdates. Eventually, this API will go away when @@ -28,6 +30,7 @@ let flushDiscreteUpdatesImpl = function() {}; let batchedEventUpdatesImpl = batchedUpdatesImpl; let isInsideEventHandler = false; +let isBatchingEventUpdates = false; function finishEventHandler() { // Here we wait until all updates have propagated, which is important @@ -60,20 +63,31 @@ export function batchedUpdates(fn, bookkeeping) { } export function batchedEventUpdates(fn, a, b) { - if (isInsideEventHandler) { + if (isBatchingEventUpdates) { // If we are currently inside another batch, we need to wait until it // fully completes before restoring state. return fn(a, b); } - isInsideEventHandler = true; + isBatchingEventUpdates = true; try { return batchedEventUpdatesImpl(fn, a, b); } finally { - isInsideEventHandler = false; + isBatchingEventUpdates = false; finishEventHandler(); } } +export function executeUserEventHandler(fn: any => void, value: any) { + const previouslyInEventHandler = isInsideEventHandler; + try { + isInsideEventHandler = true; + const type = typeof value === 'object' && value !== null ? value.type : ''; + invokeGuardedCallbackAndCatchFirstError(type, fn, undefined, value); + } finally { + isInsideEventHandler = previouslyInEventHandler; + } +} + export function discreteUpdates(fn, a, b, c) { const prevIsInsideEventHandler = isInsideEventHandler; isInsideEventHandler = true; diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 432ff1ba32e36..cc82d658b6e36 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -25,12 +25,12 @@ import { batchedEventUpdates, discreteUpdates, flushDiscreteUpdatesIfNeeded, + executeUserEventHandler, } from 'legacy-events/ReactGenericBatching'; import {enqueueStateRestore} from 'legacy-events/ReactControlledComponent'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import warning from 'shared/warning'; import {enableFlareAPI} from 'shared/ReactFeatureFlags'; -import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; import invariant from 'shared/invariant'; import { isFiberSuspenseAndTimedOut, @@ -61,12 +61,6 @@ export function setListenToResponderEventTypes( listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl; } -type EventQueueItem = {| - listener: (val: any) => void, - value: any, -|}; -type EventQueue = Array; - type ResponderTimeout = {| id: TimeoutID, timers: Map, @@ -84,15 +78,10 @@ const rootEventTypesToEventResponderInstances: Map< DOMTopLevelEventType | string, Set, > = new Map(); -const ownershipChangeListeners: Set = new Set(); - -let globalOwner = null; let currentTimeStamp = 0; let currentTimers = new Map(); let currentInstance: null | ReactDOMEventResponderInstance = null; -let currentEventQueue: null | EventQueue = null; -let currentEventQueuePriority: EventPriority = ContinuousEvent; let currentTimerIDCounter = 0; let currentDocument: null | Document = null; @@ -104,12 +93,29 @@ const eventResponderContext: ReactDOMResponderContext = { ): void { validateResponderContext(); validateEventValue(eventValue); - if (eventPriority < currentEventQueuePriority) { - currentEventQueuePriority = eventPriority; + switch (eventPriority) { + case DiscreteEvent: { + flushDiscreteUpdatesIfNeeded(currentTimeStamp); + discreteUpdates(() => + executeUserEventHandler(eventListener, eventValue), + ); + break; + } + case UserBlockingEvent: { + if (enableUserBlockingEvents) { + runWithPriority(UserBlockingPriority, () => + executeUserEventHandler(eventListener, eventValue), + ); + } else { + executeUserEventHandler(eventListener, eventValue); + } + break; + } + case ContinuousEvent: { + executeUserEventHandler(eventListener, eventValue); + break; + } } - ((currentEventQueue: any): EventQueue).push( - createEventQueueItem(eventValue, eventListener), - ); }, isTargetWithinResponder(target: Element | Document): boolean { validateResponderContext(); @@ -196,25 +202,6 @@ const eventResponderContext: ReactDOMResponderContext = { } } }, - hasOwnership(): boolean { - validateResponderContext(); - return globalOwner === currentInstance; - }, - requestGlobalOwnership(): boolean { - validateResponderContext(); - if (globalOwner !== null) { - return false; - } - globalOwner = currentInstance; - triggerOwnershipListeners(); - return true; - }, - releaseOwnership(): boolean { - validateResponderContext(); - return releaseOwnershipForEventResponderInstance( - ((currentInstance: any): ReactDOMEventResponderInstance), - ); - }, setTimeout(func: () => void, delay): number { validateResponderContext(); if (currentTimers === null) { @@ -379,16 +366,6 @@ function collectFocusableElements( } } -function createEventQueueItem( - value: any, - listener: (val: any) => void, -): EventQueueItem { - return { - value, - listener, - }; -} - function doesFiberHaveResponder( fiber: Fiber, responder: ReactDOMEventResponder, @@ -409,17 +386,6 @@ function getActiveDocument(): Document { return ((currentDocument: any): Document); } -function releaseOwnershipForEventResponderInstance( - eventResponderInstance: ReactDOMEventResponderInstance, -): boolean { - if (globalOwner === eventResponderInstance) { - globalOwner = null; - triggerOwnershipListeners(); - return true; - } - return false; -} - function isFiberHostComponentFocusable(fiber: Fiber): boolean { if (fiber.tag !== HostComponent) { return false; @@ -452,24 +418,22 @@ function processTimers( delay: number, ): void { const timersArr = Array.from(timers.values()); - currentEventQueuePriority = ContinuousEvent; try { - for (let i = 0; i < timersArr.length; i++) { - const {instance, func, id, timeStamp} = timersArr[i]; - currentInstance = instance; - currentEventQueue = []; - currentTimeStamp = timeStamp + delay; - try { - func(); - } finally { - activeTimeouts.delete(id); + batchedEventUpdates(() => { + for (let i = 0; i < timersArr.length; i++) { + const {instance, func, id, timeStamp} = timersArr[i]; + currentInstance = instance; + currentTimeStamp = timeStamp + delay; + try { + func(); + } finally { + activeTimeouts.delete(id); + } } - } - processEventQueue(); + }); } finally { currentTimers = null; currentInstance = null; - currentEventQueue = null; currentTimeStamp = 0; } } @@ -508,45 +472,6 @@ function createDOMResponderEvent( }; } -function processEvents(eventQueue: EventQueue): void { - for (let i = 0, length = eventQueue.length; i < length; i++) { - const {value, listener} = eventQueue[i]; - const type = typeof value === 'object' && value !== null ? value.type : ''; - invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, value); - } -} - -function processEventQueue(): void { - const eventQueue = ((currentEventQueue: any): EventQueue); - if (eventQueue.length === 0) { - return; - } - switch (currentEventQueuePriority) { - case DiscreteEvent: { - flushDiscreteUpdatesIfNeeded(currentTimeStamp); - discreteUpdates(() => { - batchedEventUpdates(processEvents, eventQueue); - }); - break; - } - case UserBlockingEvent: { - if (enableUserBlockingEvents) { - runWithPriority( - UserBlockingPriority, - batchedEventUpdates.bind(null, processEvents, eventQueue), - ); - } else { - batchedEventUpdates(processEvents, eventQueue); - } - break; - } - case ContinuousEvent: { - batchedEventUpdates(processEvents, eventQueue); - break; - } - } -} - function responderEventTypesContainType( eventTypes: Array, type: string, @@ -571,12 +496,6 @@ function validateResponderTargetEventTypes( return false; } -function validateOwnership( - responderInstance: ReactDOMEventResponderInstance, -): boolean { - return globalOwner === null || globalOwner === responderInstance; -} - function traverseAndHandleEventResponderInstances( topLevelType: string, targetFiber: null | Fiber, @@ -610,22 +529,19 @@ function traverseAndHandleEventResponderInstances( const responderInstances = Array.from(respondersMap.values()); for (let i = 0, length = responderInstances.length; i < length; i++) { const responderInstance = responderInstances[i]; - - if (validateOwnership(responderInstance)) { - const {props, responder, state, target} = responderInstance; - if ( - !visitedResponders.has(responder) && - validateResponderTargetEventTypes(eventType, responder) - ) { - visitedResponders.add(responder); - const onEvent = responder.onEvent; - if (onEvent !== null) { - currentInstance = responderInstance; - responderEvent.responderTarget = ((target: any): - | Element - | Document); - onEvent(responderEvent, eventResponderContext, props, state); - } + const {props, responder, state, target} = responderInstance; + if ( + !visitedResponders.has(responder) && + validateResponderTargetEventTypes(eventType, responder) + ) { + visitedResponders.add(responder); + const onEvent = responder.onEvent; + if (onEvent !== null) { + currentInstance = responderInstance; + responderEvent.responderTarget = ((target: any): + | Element + | Document); + onEvent(responderEvent, eventResponderContext, props, state); } } } @@ -642,9 +558,6 @@ function traverseAndHandleEventResponderInstances( for (let i = 0; i < responderInstances.length; i++) { const responderInstance = responderInstances[i]; - if (!validateOwnership(responderInstance)) { - continue; - } const {props, responder, state, target} = responderInstance; const onRootEvent = responder.onRootEvent; if (onRootEvent !== null) { @@ -656,51 +569,20 @@ function traverseAndHandleEventResponderInstances( } } -function triggerOwnershipListeners(): void { - const listeningInstances = Array.from(ownershipChangeListeners); - const previousInstance = currentInstance; - const previousEventQueuePriority = currentEventQueuePriority; - const previousEventQueue = currentEventQueue; - try { - for (let i = 0; i < listeningInstances.length; i++) { - const instance = listeningInstances[i]; - const {props, responder, state} = instance; - currentInstance = instance; - currentEventQueuePriority = ContinuousEvent; - currentEventQueue = []; - const onOwnershipChange = ((responder: any): ReactDOMEventResponder) - .onOwnershipChange; - if (onOwnershipChange !== null) { - onOwnershipChange(eventResponderContext, props, state); - } - } - processEventQueue(); - } finally { - currentInstance = previousInstance; - currentEventQueue = previousEventQueue; - currentEventQueuePriority = previousEventQueuePriority; - } -} - export function mountEventResponder( responder: ReactDOMEventResponder, responderInstance: ReactDOMEventResponderInstance, props: Object, state: Object, ) { - if (responder.onOwnershipChange !== null) { - ownershipChangeListeners.add(responderInstance); - } const onMount = responder.onMount; if (onMount !== null) { - currentEventQueuePriority = ContinuousEvent; currentInstance = responderInstance; - currentEventQueue = []; try { - onMount(eventResponderContext, props, state); - processEventQueue(); + batchedEventUpdates(() => { + onMount(eventResponderContext, props, state); + }); } finally { - currentEventQueue = null; currentInstance = null; currentTimers = null; } @@ -714,22 +596,16 @@ export function unmountEventResponder( const onUnmount = responder.onUnmount; if (onUnmount !== null) { let {props, state} = responderInstance; - currentEventQueue = []; - currentEventQueuePriority = ContinuousEvent; currentInstance = responderInstance; try { - onUnmount(eventResponderContext, props, state); - processEventQueue(); + batchedEventUpdates(() => { + onUnmount(eventResponderContext, props, state); + }); } finally { - currentEventQueue = null; currentInstance = null; currentTimers = null; } } - releaseOwnershipForEventResponderInstance(responderInstance); - if (responder.onOwnershipChange !== null) { - ownershipChangeListeners.delete(responderInstance); - } const rootEventTypesSet = responderInstance.rootEventTypes; if (rootEventTypesSet !== null) { const rootEventTypes = Array.from(rootEventTypesSet); @@ -762,15 +638,11 @@ export function dispatchEventForResponderEventSystem( eventSystemFlags: EventSystemFlags, ): void { if (enableFlareAPI) { - const previousEventQueue = currentEventQueue; const previousInstance = currentInstance; const previousTimers = currentTimers; const previousTimeStamp = currentTimeStamp; const previousDocument = currentDocument; - const previousEventQueuePriority = currentEventQueuePriority; currentTimers = null; - currentEventQueue = []; - currentEventQueuePriority = ContinuousEvent; // nodeType 9 is DOCUMENT_NODE currentDocument = (nativeEventTarget: any).nodeType === 9 @@ -779,21 +651,20 @@ export function dispatchEventForResponderEventSystem( // We might want to control timeStamp another way here currentTimeStamp = (nativeEvent: any).timeStamp; try { - traverseAndHandleEventResponderInstances( - topLevelType, - targetFiber, - nativeEvent, - nativeEventTarget, - eventSystemFlags, - ); - processEventQueue(); + batchedEventUpdates(() => { + traverseAndHandleEventResponderInstances( + topLevelType, + targetFiber, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + }); } finally { currentTimers = previousTimers; currentInstance = previousInstance; - currentEventQueue = previousEventQueue; currentTimeStamp = previousTimeStamp; currentDocument = previousDocument; - currentEventQueuePriority = previousEventQueuePriority; } } } diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index 14c52faefd48f..80f3a0a492b5c 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -26,7 +26,6 @@ function createEventResponder({ targetEventTypes, onMount, onUnmount, - onOwnershipChange, getInitialState, }) { return React.unstable_createResponder('TestEventResponder', { @@ -36,7 +35,6 @@ function createEventResponder({ onRootEvent, onMount, onUnmount, - onOwnershipChange, getInitialState, }); } @@ -644,37 +642,6 @@ describe('DOMEventResponderSystem', () => { expect(counter).toEqual(5); }); - it('the event responder onOwnershipChange() function should fire', () => { - let onOwnershipChangeFired = 0; - let ownershipGained = false; - const buttonRef = React.createRef(); - - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props, state) => { - ownershipGained = context.requestGlobalOwnership(); - }, - onOwnershipChange: () => { - onOwnershipChangeFired++; - }, - }); - - const Test = () => { - const listener = React.unstable_useResponder(TestResponder, {}); - return