Refactor EventComponent logic + add onOwnershipChange callback (#15354)

This commit is contained in:
Dominic Gannaway
2019-04-09 12:47:32 +01:00
committed by GitHub
parent 865c04b0c7
commit 8a9f31eb53
13 changed files with 229 additions and 117 deletions
+10 -6
View File
@@ -11,6 +11,7 @@ import * as Scheduler from 'scheduler';
import invariant from 'shared/invariant';
import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals';
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
// Intentionally not named imports because Rollup would
// use dynamic dispatch for CommonJS interop named imports.
@@ -439,17 +440,20 @@ export function unhideTextInstance(textInstance, text): void {
// Noop
}
export function handleEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
export function mountEventComponent(
eventComponentInstance: ReactEventComponentInstance,
) {
throw new Error('Not yet implemented.');
}
export function updateEventComponent(
eventComponentInstance: ReactEventComponentInstance,
) {
throw new Error('Not yet implemented.');
}
export function unmountEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
internalInstanceHandle: Object,
eventComponentInstance: ReactEventComponentInstance,
): void {
throw new Error('Not yet implemented.');
}
+20 -10
View File
@@ -44,8 +44,11 @@ import {
import dangerousStyleValue from '../shared/dangerousStyleValue';
import type {DOMContainer} from './ReactDOM';
import type {ReactEventResponder} from 'shared/ReactTypes';
import {unmountEventResponder} from '../events/DOMEventResponderSystem';
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
import {
mountEventResponder,
unmountEventResponder,
} from '../events/DOMEventResponderSystem';
import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols';
import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -888,27 +891,34 @@ export function didNotFindHydratableSuspenseInstance(
}
}
export function handleEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
export function mountEventComponent(
eventComponentInstance: ReactEventComponentInstance,
): void {
if (enableEventAPI) {
mountEventResponder(eventComponentInstance);
updateEventComponent(eventComponentInstance);
}
}
export function updateEventComponent(
eventComponentInstance: ReactEventComponentInstance,
): void {
if (enableEventAPI) {
const rootContainerInstance = ((eventComponentInstance.rootInstance: any): Container);
const rootElement = rootContainerInstance.ownerDocument;
listenToEventResponderEventTypes(
eventResponder.targetEventTypes,
eventComponentInstance.responder.targetEventTypes,
rootElement,
);
}
}
export function unmountEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
internalInstanceHandle: Object,
eventComponentInstance: ReactEventComponentInstance,
): void {
if (enableEventAPI) {
// TODO stop listening to targetEventTypes
unmountEventResponder(eventResponder, internalInstanceHandle);
unmountEventResponder(eventComponentInstance);
}
}
+76 -45
View File
@@ -22,8 +22,8 @@ import {
EventTarget as EventTargetWorkTag,
} from 'shared/ReactWorkTags';
import type {
ReactEventResponder,
ReactEventResponderEventType,
ReactEventComponentInstance,
} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
@@ -55,7 +55,7 @@ type PartialEventObject = {
};
let currentOwner = null;
let currentFiber: Fiber;
let currentInstance: ReactEventComponentInstance;
let currentEventQueue: EventQueue;
const eventResponderContext: ResponderContext = {
@@ -140,12 +140,10 @@ const eventResponderContext: ResponderContext = {
return false;
},
isTargetWithinEventComponent(target: Element | Document): boolean {
const eventFiber = currentFiber;
if (target != null) {
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber === eventFiber || fiber === eventFiber.alternate) {
if (fiber.stateNode === currentInstance) {
return true;
}
fiber = fiber.return;
@@ -174,78 +172,78 @@ const eventResponderContext: ResponderContext = {
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
listenToResponderEventTypesImpl(rootEventTypes, doc);
const eventComponent = currentFiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
topLevelEventType,
);
if (rootEventComponents === undefined) {
rootEventComponents = new Set();
rootEventTypesToEventComponents.set(
if (rootEventComponentInstances === undefined) {
rootEventComponentInstances = new Set();
rootEventTypesToEventComponentInstances.set(
topLevelEventType,
rootEventComponents,
rootEventComponentInstances,
);
}
rootEventComponents.add(eventComponent);
rootEventComponentInstances.add(currentInstance);
}
},
removeRootEventTypes(
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
const eventComponent = currentFiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
let rootEventComponents = rootEventTypesToEventComponentInstances.get(
topLevelEventType,
);
if (rootEventComponents !== undefined) {
rootEventComponents.delete(eventComponent);
rootEventComponents.delete(currentInstance);
}
}
},
hasOwnership(): boolean {
return currentOwner === currentFiber;
return currentOwner === currentInstance;
},
requestOwnership(): boolean {
if (currentOwner !== null) {
return false;
}
currentOwner = currentFiber;
currentOwner = currentInstance;
triggerOwnershipListeners();
return true;
},
releaseOwnership(): boolean {
if (currentOwner !== currentFiber) {
if (currentOwner !== currentInstance) {
return false;
}
currentOwner = null;
triggerOwnershipListeners();
return false;
},
setTimeout(func: () => void, delay): TimeoutID {
const contextFiber = currentFiber;
const contextInstance = currentInstance;
return setTimeout(() => {
const previousEventQueue = currentEventQueue;
const previousFiber = currentFiber;
const previousInstance = currentInstance;
currentEventQueue = createEventQueue();
currentFiber = contextFiber;
currentInstance = contextInstance;
try {
func();
batchedUpdates(processEventQueue, currentEventQueue);
} finally {
currentFiber = previousFiber;
currentInstance = previousInstance;
currentEventQueue = previousEventQueue;
}
}, delay);
},
};
const rootEventTypesToEventComponents: Map<
const rootEventTypesToEventComponentInstances: Map<
DOMTopLevelEventType | string,
Set<Fiber>,
Set<ReactEventComponentInstance>,
> = new Map();
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
const eventsWithStopPropagation:
@@ -255,6 +253,7 @@ const targetEventTypeCached: Map<
Array<ReactEventResponderEventType>,
Set<DOMTopLevelEventType>,
> = new Map();
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();
function createResponderEvent(
topLevelType: string,
@@ -343,11 +342,11 @@ function getTargetEventTypes(
function handleTopLevelType(
topLevelType: DOMTopLevelEventType,
fiber: Fiber,
responderEvent: ResponderEvent,
eventComponentInstance: ReactEventComponentInstance,
isRootLevelEvent: boolean,
): void {
const responder: ReactEventResponder = fiber.type.responder;
let {props, responder, state} = eventComponentInstance;
if (!isRootLevelEvent) {
// Validate the target event type exists on the responder
const targetEventTypes = getTargetEventTypes(responder.targetEventTypes);
@@ -355,13 +354,12 @@ function handleTopLevelType(
return;
}
}
let {props, state} = fiber.stateNode;
const previousFiber = currentFiber;
currentFiber = fiber;
const previousInstance = currentInstance;
currentInstance = eventComponentInstance;
try {
responder.onEvent(responderEvent, eventResponderContext, props, state);
} finally {
currentFiber = previousFiber;
currentInstance = previousInstance;
}
}
@@ -384,23 +382,29 @@ export function runResponderEventsInBatch(
// Traverse up the fiber tree till we find event component fibers.
while (node !== null) {
if (node.tag === EventComponent) {
handleTopLevelType(topLevelType, node, responderEvent, false);
const eventComponentInstance = node.stateNode;
handleTopLevelType(
topLevelType,
responderEvent,
eventComponentInstance,
false,
);
}
node = node.return;
}
// Handle root level events
const rootEventComponents = rootEventTypesToEventComponents.get(
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
topLevelType,
);
if (rootEventComponents !== undefined) {
const rootEventComponentFibers = Array.from(rootEventComponents);
if (rootEventInstances !== undefined) {
const rootEventComponentInstances = Array.from(rootEventInstances);
for (let i = 0; i < rootEventComponentFibers.length; i++) {
const rootEventComponentFiber = rootEventComponentFibers[i];
for (let i = 0; i < rootEventComponentInstances.length; i++) {
const rootEventComponentInstance = rootEventComponentInstances[i];
handleTopLevelType(
topLevelType,
rootEventComponentFiber,
responderEvent,
rootEventComponentInstance,
true,
);
}
@@ -409,26 +413,53 @@ export function runResponderEventsInBatch(
}
}
function triggerOwnershipListeners(): void {
const listeningInstances = Array.from(ownershipChangeListeners);
const previousInstance = currentInstance;
for (let i = 0; i < listeningInstances.length; i++) {
const instance = listeningInstances[i];
const {props, responder, state} = instance;
currentInstance = instance;
try {
responder.onOwnershipChange(eventResponderContext, props, state);
} finally {
currentInstance = previousInstance;
}
}
}
export function mountEventResponder(
eventComponentInstance: ReactEventComponentInstance,
) {
const responder = eventComponentInstance.responder;
if (responder.onOwnershipChange !== undefined) {
ownershipChangeListeners.add(eventComponentInstance);
}
}
export function unmountEventResponder(
responder: ReactEventResponder,
fiber: Fiber,
eventComponentInstance: ReactEventComponentInstance,
): void {
const responder = eventComponentInstance.responder;
const onUnmount = responder.onUnmount;
if (onUnmount !== undefined) {
let {props, state} = fiber.stateNode;
let {props, state} = eventComponentInstance;
const previousEventQueue = currentEventQueue;
const previousFiber = currentFiber;
const previousInstance = currentInstance;
currentEventQueue = createEventQueue();
currentFiber = fiber;
currentInstance = eventComponentInstance;
try {
onUnmount(eventResponderContext, props, state);
} finally {
currentEventQueue = previousEventQueue;
currentFiber = previousFiber;
currentInstance = previousInstance;
}
}
if (currentOwner === fiber) {
// TODO fire owner changed callback
if (currentOwner === eventComponentInstance) {
currentOwner = null;
triggerOwnershipListeners();
}
if (responder.onOwnershipChange !== undefined) {
ownershipChangeListeners.delete(eventComponentInstance);
}
}
@@ -18,12 +18,14 @@ function createReactEventComponent(
createInitialState,
onEvent,
onUnmount,
onOwnershipChange,
) {
const testEventResponder = {
targetEventTypes,
createInitialState,
onEvent,
onUnmount,
onOwnershipChange,
};
return {
@@ -378,4 +380,38 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(null, container);
expect(counter).toEqual(5);
});
it('the event responder onOwnershipChange() function should fire', () => {
let onOwnershipChangeFired = 0;
let ownershipGained = false;
const buttonRef = React.createRef();
const EventComponent = createReactEventComponent(
['click'],
undefined,
(event, context, props, state) => {
ownershipGained = context.requestOwnership();
},
undefined,
() => {
onOwnershipChangeFired++;
},
);
const Test = () => (
<EventComponent>
<button ref={buttonRef} />
</EventComponent>
);
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);
});
});
+10 -7
View File
@@ -14,7 +14,7 @@ import type {
NativeMethodsMixinType,
ReactNativeBaseComponentViewConfig,
} from './ReactNativeTypes';
import type {ReactEventResponder} from 'shared/ReactTypes';
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
import {mountSafeCallback_NOT_REALLY_SAFE} from './NativeMethodsMixinUtils';
import {create, diff} from './ReactNativeAttributePayload';
@@ -434,17 +434,20 @@ export function replaceContainerChildren(
newChildren: ChildSet,
): void {}
export function handleEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
export function mountEventComponent(
eventComponentInstance: ReactEventComponentInstance,
) {
throw new Error('Not yet implemented.');
}
export function updateEventComponent(
eventComponentInstance: ReactEventComponentInstance,
) {
throw new Error('Not yet implemented.');
}
export function unmountEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
internalInstanceHandle: Object,
eventComponentInstance: ReactEventComponentInstance,
): void {
throw new Error('Not yet implemented.');
}
+10 -7
View File
@@ -8,7 +8,7 @@
*/
import type {ReactNativeBaseComponentViewConfig} from './ReactNativeTypes';
import type {ReactEventResponder} from 'shared/ReactTypes';
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
import invariant from 'shared/invariant';
@@ -493,17 +493,20 @@ export function unhideTextInstance(
throw new Error('Not yet implemented.');
}
export function handleEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
export function mountEventComponent(
eventComponentInstance: ReactEventComponentInstance,
) {
throw new Error('Not yet implemented.');
}
export function updateEventComponent(
eventComponentInstance: ReactEventComponentInstance,
) {
throw new Error('Not yet implemented.');
}
export function unmountEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
internalInstanceHandle: Object,
eventComponentInstance: ReactEventComponentInstance,
): void {
throw new Error('Not yet implemented.');
}
+5 -1
View File
@@ -431,7 +431,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
isPrimaryRenderer: true,
supportsHydration: false,
handleEventComponent(): void {
mountEventComponent(): void {
// NO-OP
},
updateEventComponent(): void {
// NO-OP
},
-6
View File
@@ -623,12 +623,6 @@ export function createFiberFromEventComponent(
const fiber = createFiber(EventComponent, pendingProps, key, mode);
fiber.elementType = eventComponent;
fiber.type = eventComponent;
fiber.stateNode = {
context: null,
props: pendingProps,
rootInstance: null,
state: null,
};
fiber.expirationTime = expirationTime;
return fiber;
}
+2 -3
View File
@@ -749,9 +749,8 @@ function commitUnmount(current: Fiber): void {
}
case EventComponent: {
if (enableEventAPI) {
const rootContainerInstance = current.stateNode.rootInstance;
const responder = current.type.responder;
unmountEventComponent(responder, rootContainerInstance, current);
const eventComponentInstance = current.stateNode;
unmountEventComponent(eventComponentInstance);
current.stateNode = null;
}
}
+25 -13
View File
@@ -17,6 +17,7 @@ import type {
Container,
ChildSet,
} from './ReactFiberHostConfig';
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
import {
IndeterminateComponent,
@@ -65,7 +66,8 @@ import {
createContainerChildSet,
appendChildToContainerChildSet,
finalizeContainerChildren,
handleEventComponent,
mountEventComponent,
updateEventComponent,
handleEventTarget,
} from './ReactFiberHostConfig';
import {
@@ -774,19 +776,29 @@ function completeWork(
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const responder = workInProgress.type.responder;
const stateNode = workInProgress.stateNode;
// Update the props on the event component state node
stateNode.props = newProps;
// Update the root container, so we can properly unmount events at some point
stateNode.rootInstance = rootContainerInstance;
// Initialize event component state if createInitialState exists
if (
stateNode.state === null &&
responder.createInitialState !== undefined
) {
stateNode.state = responder.createInitialState(newProps);
let eventComponentInstance: ReactEventComponentInstance | null =
workInProgress.stateNode;
if (eventComponentInstance === null) {
let responderState = null;
if (responder.createInitialState !== undefined) {
responderState = responder.createInitialState(newProps);
}
eventComponentInstance = workInProgress.stateNode = {
context: null,
props: newProps,
responder,
rootInstance: rootContainerInstance,
state: responderState,
};
mountEventComponent(eventComponentInstance);
} else {
// Update the props on the event component state node
eventComponentInstance.props = newProps;
// Update the root container, so we can properly unmount events at some point
eventComponentInstance.rootInstance = rootContainerInstance;
updateEventComponent(eventComponentInstance);
}
handleEventComponent(responder, rootContainerInstance);
}
break;
}
@@ -63,7 +63,8 @@ export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer;
export const supportsMutation = $$$hostConfig.supportsMutation;
export const supportsPersistence = $$$hostConfig.supportsPersistence;
export const supportsHydration = $$$hostConfig.supportsHydration;
export const handleEventComponent = $$$hostConfig.handleEventComponent;
export const mountEventComponent = $$$hostConfig.mountEventComponent;
export const updateEventComponent = $$$hostConfig.updateEventComponent;
export const handleEventTarget = $$$hostConfig.handleEventTarget;
export const getEventTargetChildElement =
$$$hostConfig.getEventTargetChildElement;
+15 -12
View File
@@ -9,7 +9,7 @@
import warning from 'shared/warning';
import type {ReactEventResponder} from 'shared/ReactTypes';
import type {ReactEventComponentInstance} from 'shared/ReactTypes';
import {REACT_EVENT_TARGET_TOUCH_HIT} from 'shared/ReactSymbols';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
@@ -327,9 +327,20 @@ export function unhideTextInstance(
textInstance.isHidden = false;
}
export function handleEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
export function mountEventComponent(
eventComponentInstance: ReactEventComponentInstance,
): void {
// noop
}
export function updateEventComponent(
eventComponentInstance: ReactEventComponentInstance,
): void {
// noop
}
export function unmountEventComponent(
eventComponentInstance: ReactEventComponentInstance,
): void {
// noop
}
@@ -363,14 +374,6 @@ export function getEventTargetChildElement(
return null;
}
export function unmountEventComponent(
eventResponder: ReactEventResponder,
rootContainerInstance: Container,
internalInstanceHandle: Object,
): void {
// TODO: add unmountEventComponent implementation
}
export function handleEventTarget(
type: Symbol | number,
props: Props,
+18 -6
View File
@@ -89,21 +89,33 @@ export type ReactEventResponderEventType =
export type ReactEventResponder = {
targetEventTypes: Array<ReactEventResponderEventType>,
createInitialState?: (props: Object) => Object,
createInitialState?: (props: null | Object) => Object,
onEvent: (
event: ResponderEvent,
context: ResponderContext,
props: Object,
state: Object,
props: null | Object,
state: null | Object,
) => void,
onUnmount: (
context: ResponderContext,
props: null | Object,
state: null | Object,
) => void,
onUnmount: (context: ResponderContext, props: Object, state: Object) => void,
onOwnershipChange: (
context: ResponderContext,
props: Object,
state: Object,
props: null | Object,
state: null | Object,
) => void,
};
export type ReactEventComponentInstance = {|
context: null | Object,
props: null | Object,
responder: ReactEventResponder,
rootInstance: mixed,
state: null | Object,
|};
export type ReactEventComponent = {|
$$typeof: Symbol | number,
displayName?: string,