[Flare] Rework the responder dispatching/batching mechanism (#16334)

This commit is contained in:
Dominic Gannaway
2019-08-19 19:22:46 +01:00
committed by GitHub
parent 6ae6a7c020
commit dce430ad92
15 changed files with 149 additions and 565 deletions

View File

@@ -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;

View File

@@ -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<EventQueueItem>;
type ResponderTimeout = {|
id: TimeoutID,
timers: Map<number, ResponderTimer>,
@@ -84,15 +78,10 @@ const rootEventTypesToEventResponderInstances: Map<
DOMTopLevelEventType | string,
Set<ReactDOMEventResponderInstance>,
> = new Map();
const ownershipChangeListeners: Set<ReactDOMEventResponderInstance> = 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<string>,
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;
}
}
}

View File

@@ -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 <button ref={buttonRef} listeners={listener} />;
};
ReactDOM.render(<Test />, container);
// Clicking the button should trigger the event responder onEvent()
let buttonElement = buttonRef.current;
dispatchClickEvent(buttonElement);
jest.runAllTimers();
expect(ownershipGained).toEqual(true);
expect(onOwnershipChangeFired).toEqual(1);
});
it('the event responder root listeners should fire on a root click event', () => {
let eventResponderFiredCount = 0;
let eventLog = [];

View File

@@ -50,10 +50,6 @@ elements within the Event Responder.
Called after an Event Responder in mounted.
### onOwnershipChange?: (context: ResponderContext, props, state)
Called when ownership is granted or terminated (either globally or for the responder) for an Event Responder instance.
### onRootEvent?: (event: ResponderEvent, context: ResponderContext, props, state)
Called when any of the `rootEventTypes` are dispatched on the root of the app.
@@ -101,10 +97,6 @@ context.dispatchEvent('onPress', event, DiscreteEvent);
Returns every DOM element that can be focused within the scope of the Event
Responder instance.
### hasOwnership(): boolean
Returns `true` if the instance has taken ownership of the responder.
### isTargetWithinNode(target: Element, element: Element): boolean
Returns `true` if `target` is a child of `element`.
@@ -118,21 +110,10 @@ Returns `true` is the target element is within the subtree of the Event Responde
Returns `true` is the target element is within the current Event Responder's scope. If the target element
is within the scope of the same responder, but owned by another Event Responder instance, this will return `false`.
### releaseOwnership(): boolean
Returns `true` if the instance released ownership of the Event Responder instance.
### removeRootEventTypes(eventTypes: Array<ResponderEventType>)
Remove the root event types added with `addRootEventTypes`.
### requestGlobalOwnership(): boolean
The current Event Responder instance can request global ownership of the event system. When an Event Responder instance
has global ownership, only that instance and its responder are active. To release ownership to other event responders,
either `releaseOwnership()` must be called or the Event Responder instance that had global ownership must be
unmounted. Calling `requestGlobalOwnership` also returns `true`/`false` if the request was successful.
### setTimeout(func: () => void, delay: number): Symbol
This can be used to dispatch async events, e.g., those that fire after a delay.

View File

@@ -39,7 +39,6 @@ type DragState = {|
startY: number,
x: number,
y: number,
ownershipClaimed: boolean,
|};
// In the case we don't have PointerEvents (Safari), we listen to touch events
@@ -111,7 +110,6 @@ const dragResponderImpl = {
startY: 0,
x: 0,
y: 0,
ownershipClaimed: false,
};
},
onEvent(
@@ -182,24 +180,10 @@ const dragResponderImpl = {
return;
}
if (!state.isDragging) {
let shouldEnableDragging = true;
if (props.shouldClaimOwnership && props.shouldClaimOwnership()) {
shouldEnableDragging = context.requestGlobalOwnership();
if (shouldEnableDragging) {
state.ownershipClaimed = true;
}
}
if (shouldEnableDragging) {
state.isDragging = true;
const onDragChange = props.onDragChange;
if (isFunction(onDragChange)) {
context.dispatchEvent(true, onDragChange, UserBlockingEvent);
}
} else {
state.dragTarget = null;
state.isPointerDown = false;
context.removeRootEventTypes(rootEventTypes);
state.isDragging = true;
const onDragChange = props.onDragChange;
if (isFunction(onDragChange)) {
context.dispatchEvent(true, onDragChange, UserBlockingEvent);
}
} else {
const onDragMove = props.onDragMove;
@@ -228,9 +212,6 @@ const dragResponderImpl = {
case 'mouseup':
case 'pointerup': {
if (state.isDragging) {
if (state.ownershipClaimed) {
context.releaseOwnership();
}
const onDragEnd = props.onDragEnd;
if (isFunction(onDragEnd)) {
dispatchDragEvent(

View File

@@ -341,13 +341,6 @@ const focusResponderImpl = {
) {
unmountFocusResponder(context, props, state);
},
onOwnershipChange(
context: ReactDOMResponderContext,
props: FocusProps,
state: FocusState,
) {
unmountFocusResponder(context, props, state);
},
};
export const FocusResponder = React.unstable_createResponder(
@@ -485,13 +478,6 @@ const focusWithinResponderImpl = {
) {
unmountFocusWithinResponder(context, props, state);
},
onOwnershipChange(
context: ReactDOMResponderContext,
props: FocusWithinProps,
state: FocusState,
) {
unmountFocusWithinResponder(context, props, state);
},
};
export const FocusWithinResponder = React.unstable_createResponder(

View File

@@ -260,7 +260,6 @@ const hoverResponderImpl = {
}
},
onUnmount: unmountResponder,
onOwnershipChange: unmountResponder,
};
const hoverResponderFallbackImpl = {
@@ -335,7 +334,6 @@ const hoverResponderFallbackImpl = {
}
},
onUnmount: unmountResponder,
onOwnershipChange: unmountResponder,
};
export const HoverResponder = React.unstable_createResponder(

View File

@@ -841,13 +841,6 @@ const pressResponderImpl = {
) {
unmountResponder(context, props, state);
},
onOwnershipChange(
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
) {
unmountResponder(context, props, state);
},
};
export const PressResponder = React.unstable_createResponder(

View File

@@ -303,13 +303,6 @@ const scrollResponderImpl = {
) {
// TODO
},
onOwnershipChange(
context: ReactDOMResponderContext,
props: ScrollProps,
state: ScrollState,
) {
// TODO
},
};
export const ScrollResponder = React.unstable_createResponder(

View File

@@ -101,7 +101,6 @@ type SwipeState = {
swipeTarget: null | Element | Document,
x: number,
y: number,
ownershipClaimed: boolean,
};
const swipeResponderImpl = {
@@ -117,7 +116,6 @@ const swipeResponderImpl = {
swipeTarget: null,
x: 0,
y: 0,
ownershipClaimed: false,
};
},
onEvent(
@@ -141,25 +139,13 @@ const swipeResponderImpl = {
const x = (obj: any).screenX;
const y = (obj: any).screenY;
let shouldEnableSwiping = true;
if (props.shouldClaimOwnership && props.shouldClaimOwnership()) {
shouldEnableSwiping = context.requestGlobalOwnership();
if (shouldEnableSwiping) {
state.ownershipClaimed = true;
}
}
if (shouldEnableSwiping) {
state.isSwiping = true;
state.startX = x;
state.startY = y;
state.x = x;
state.y = y;
state.swipeTarget = target;
context.addRootEventTypes(rootEventTypes);
} else {
state.touchId = null;
}
state.isSwiping = true;
state.startX = x;
state.startY = y;
state.x = x;
state.y = y;
state.swipeTarget = target;
context.addRootEventTypes(rootEventTypes);
}
break;
}
@@ -238,9 +224,6 @@ const swipeResponderImpl = {
if (state.x === state.startX && state.y === state.startY) {
return;
}
if (state.ownershipClaimed) {
context.releaseOwnership();
}
const direction = state.direction;
const lastDirection = state.lastDirection;
if (direction !== lastDirection) {

View File

@@ -141,61 +141,6 @@ describe('Drag event responder', () => {
expect(events).toEqual(['dragstart', 'dragend']);
});
it('should support onDragStart and onDragEnd with ownership', () => {
let divRef = React.createRef();
let events = [];
function handleDragStart() {
events.push('dragstart');
}
function handleDragEnd() {
events.push('dragend');
}
function Component() {
const listener = useDragResponder({
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
shouldClaimOwnership: () => true,
});
return (
<div ref={divRef} listeners={listener}>
Drag me!
</div>
);
}
ReactDOM.render(<Component />, container);
const mouseOverEvent = document.createEvent('MouseEvents');
mouseOverEvent.initEvent('mousedown', true, true);
divRef.current.dispatchEvent(mouseOverEvent);
const mouseMoveEvent = document.createEvent('MouseEvents');
for (let index = 0; index <= 20; index++) {
mouseMoveEvent.initMouseEvent(
'mousemove',
true,
true,
window,
1,
index,
index,
50,
50,
);
divRef.current.dispatchEvent(mouseMoveEvent);
}
divRef.current.dispatchEvent(mouseMoveEvent);
const mouseUpEvent = document.createEvent('MouseEvents');
mouseUpEvent.initEvent('mouseup', true, true);
divRef.current.dispatchEvent(mouseUpEvent);
expect(events).toEqual(['dragstart', 'dragend']);
});
it('should support onDragMove', () => {
let divRef = React.createRef();
let events = [];

View File

@@ -13,6 +13,7 @@ import {
batchedEventUpdates,
discreteUpdates,
flushDiscreteUpdatesIfNeeded,
executeUserEventHandler,
} from 'legacy-events/ReactGenericBatching';
import type {
ReactEventResponder,
@@ -30,7 +31,6 @@ import {
UserBlockingEvent,
DiscreteEvent,
} from './ReactNativeTypes';
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
import {enableUserBlockingEvents} from 'shared/ReactFeatureFlags';
import warning from 'shared/warning';
import invariant from 'shared/invariant';
@@ -43,12 +43,6 @@ const {
unstable_runWithPriority: runWithPriority,
} = Scheduler;
type EventQueueItem = {|
listener: (val: any) => void,
value: any,
|};
type EventQueue = Array<EventQueueItem>;
type ResponderTimeout = {|
id: TimeoutID,
timers: Map<number, ResponderTimer>,
@@ -78,17 +72,10 @@ const rootEventTypesToEventResponderInstances: Map<
string,
Set<ReactNativeEventResponderInstance>,
> = new Map();
const ownershipChangeListeners: Set<
ReactNativeEventResponderInstance,
> = new Set();
let globalOwner = null;
let currentTimeStamp = 0;
let currentTimers = new Map();
let currentInstance: null | ReactNativeEventResponderInstance = null;
let currentEventQueue: null | EventQueue = null;
let currentEventQueuePriority: EventPriority = ContinuousEvent;
let currentTimerIDCounter = 0;
const eventResponderContext: ReactNativeResponderContext = {
@@ -99,12 +86,29 @@ const eventResponderContext: ReactNativeResponderContext = {
): 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),
);
},
isTargetWithinNode(
childTarget: ReactNativeEventTarget,
@@ -215,16 +219,6 @@ const eventResponderContext: ReactNativeResponderContext = {
},
};
function createEventQueueItem(
value: any,
listener: (val: any) => void,
): EventQueueItem {
return {
value,
listener,
};
}
function validateEventValue(eventValue: any): void {
if (typeof eventValue === 'object' && eventValue !== null) {
const {target, type, timeStamp} = eventValue;
@@ -290,24 +284,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;
}
}
@@ -327,68 +319,12 @@ function createFabricResponderEvent(
function validateResponderContext(): void {
invariant(
currentEventQueue && currentInstance,
currentInstance,
'An event responder context was used outside of an event cycle. ' +
'Use context.setTimeout() to use asynchronous responder context outside of event cycle .',
);
}
// TODO this function is almost an exact copy of the DOM version, we should
// somehow share the logic
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;
}
}
}
// TODO this function is almost an exact copy of the DOM version, we should
// somehow share the logic
function releaseOwnershipForEventResponderInstance(
eventResponderInstance: ReactNativeEventResponderInstance,
): boolean {
if (globalOwner === eventResponderInstance) {
globalOwner = null;
triggerOwnershipListeners();
return true;
}
return false;
}
// TODO this function is almost an exact copy of the DOM version, we should
// somehow share the logic
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);
}
}
// TODO this function is almost an exact copy of the DOM version, we should
// somehow share the logic
function responderEventTypesContainType(
@@ -415,12 +351,6 @@ function validateResponderTargetEventTypes(
return false;
}
function validateOwnership(
responderInstance: ReactNativeEventResponderInstance,
): boolean {
return globalOwner === null || globalOwner === responderInstance;
}
// TODO this function is almost an exact copy of the DOM version, we should
// somehow share the logic
function traverseAndHandleEventResponderInstances(
@@ -449,20 +379,17 @@ 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)
) {
const onEvent = responder.onEvent;
visitedResponders.add(responder);
if (onEvent !== null) {
currentInstance = responderInstance;
responderEvent.responderTarget = ((target: any): ReactNativeEventTarget);
onEvent(responderEvent, eventResponderContext, props, state);
}
const {props, responder, state, target} = responderInstance;
if (
!visitedResponders.has(responder) &&
validateResponderTargetEventTypes(eventType, responder)
) {
const onEvent = responder.onEvent;
visitedResponders.add(responder);
if (onEvent !== null) {
currentInstance = responderInstance;
responderEvent.responderTarget = ((target: any): ReactNativeEventTarget);
onEvent(responderEvent, eventResponderContext, props, state);
}
}
}
@@ -479,9 +406,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) {
@@ -500,57 +424,24 @@ export function dispatchEventForResponderEventSystem(
targetFiber: null | Fiber,
nativeEvent: ReactFaricEvent,
): void {
const previousEventQueue = currentEventQueue;
const previousInstance = currentInstance;
const previousTimers = currentTimers;
const previousTimeStamp = currentTimeStamp;
const previousEventQueuePriority = currentEventQueuePriority;
currentTimers = null;
currentEventQueue = [];
currentEventQueuePriority = ContinuousEvent;
// We might want to control timeStamp another way here
currentTimeStamp = Date.now();
try {
traverseAndHandleEventResponderInstances(
topLevelType,
targetFiber,
nativeEvent,
);
processEventQueue();
batchedEventUpdates(() => {
traverseAndHandleEventResponderInstances(
topLevelType,
targetFiber,
nativeEvent,
);
});
} finally {
currentTimers = previousTimers;
currentInstance = previousInstance;
currentEventQueue = previousEventQueue;
currentTimeStamp = previousTimeStamp;
currentEventQueuePriority = previousEventQueuePriority;
}
}
// TODO this function is almost an exact copy of the DOM version, we should
// somehow share the logic
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): ReactNativeEventResponder)
.onOwnershipChange;
if (onOwnershipChange !== null) {
onOwnershipChange(eventResponderContext, props, state);
}
}
processEventQueue();
} finally {
currentInstance = previousInstance;
currentEventQueue = previousEventQueue;
currentEventQueuePriority = previousEventQueuePriority;
}
}
@@ -562,19 +453,14 @@ export function mountEventResponder(
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;
}
@@ -590,22 +476,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);

View File

@@ -64,9 +64,6 @@ export type ReactDOMResponderContext = {
isTargetWithinResponderScope: (Element | Document) => boolean,
addRootEventTypes: (rootEventTypes: Array<string>) => void,
removeRootEventTypes: (rootEventTypes: Array<string>) => void,
hasOwnership: () => boolean,
requestGlobalOwnership: () => boolean,
releaseOwnership: () => boolean,
setTimeout: (func: () => void, timeout: number) => number,
clearTimeout: (timerId: number) => void,
getFocusableElementsInScope(deep: boolean): Array<HTMLElement>,

View File

@@ -107,9 +107,6 @@ export type ReactEventResponder<E, C> = {
| ((event: E, context: C, props: Object, state: Object) => void),
onMount: null | ((context: C, props: Object, state: Object) => void),
onUnmount: null | ((context: C, props: Object, state: Object) => void),
onOwnershipChange:
| null
| ((context: C, props: Object, state: Object) => void),
};
export type EventPriority = 0 | 1 | 2;

View File

@@ -19,7 +19,6 @@ export default function createEventResponder<E, C>(
onEvent,
onMount,
onUnmount,
onOwnershipChange,
onRootEvent,
rootEventTypes,
targetEventTypes,
@@ -30,7 +29,6 @@ export default function createEventResponder<E, C>(
getInitialState: getInitialState || null,
onEvent: onEvent || null,
onMount: onMount || null,
onOwnershipChange: onOwnershipChange || null,
onRootEvent: onRootEvent || null,
onUnmount: onUnmount || null,
rootEventTypes: rootEventTypes || null,