Refactor Enter/Leave listener accumulation (#18405)

* Refactor Enter/Leave listener accumulation
This commit is contained in:
Dominic Gannaway 2020-04-02 11:07:20 +01:00 committed by GitHub
parent c80cd8ee27
commit 663c13d71d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 376 additions and 239 deletions

View File

@ -1,76 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
import getListener from 'legacy-events/getListener';
import {traverseEnterLeave} from 'react-reconciler/src/ReactTreeTraversal';
import accumulateInto from './accumulateInto';
import forEachAccumulated from './forEachAccumulated';
/**
* A small set of propagation patterns, each of which will accept a small amount
* of information, and generate a set of "dispatch ready event objects" - which
* are sets of events that have already been annotated with a set of dispatched
* listener functions/ids. The API is designed this way to discourage these
* propagation strategies from actually executing the dispatches, since we
* always want to collect the entire set of dispatches before executing even a
* single one.
*/
/**
* Accumulates without regard to direction, does not look for phased
* registration names. Same as `accumulateDirectDispatchesSingle` but without
* requiring that the `dispatchMarker` be the same as the dispatched ID.
*/
function accumulateDispatches(
inst: Fiber,
ignoredDirection: ?boolean,
event: ReactSyntheticEvent,
): void {
if (inst && event && event.dispatchConfig.registrationName) {
const registrationName = event.dispatchConfig.registrationName;
const listener = getListener(inst, registrationName);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
}
/**
* Accumulates dispatches on an `SyntheticEvent`, but only for the
* `dispatchMarker`.
* @param {SyntheticEvent} event
*/
function accumulateDirectDispatchesSingle(event: ReactSyntheticEvent) {
if (event && event.dispatchConfig.registrationName) {
accumulateDispatches(event._targetInst, null, event);
}
}
export function accumulateEnterLeaveDispatches(
leave: ReactSyntheticEvent,
enter: ReactSyntheticEvent,
from: Fiber,
to: Fiber,
) {
traverseEnterLeave(from, to, accumulateDispatches, leave, enter);
}
export function accumulateDirectDispatches(
events: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent),
) {
forEachAccumulated(events, accumulateDirectDispatchesSingle);
}

View File

@ -27,6 +27,7 @@ export type CustomDispatchConfig = {|
bubbled: null,
captured: null,
|},
registrationName?: string,
customEvent: true,
|};

View File

@ -5,20 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {
getLowestCommonAncestor,
isAncestor,
getParentInstance,
traverseTwoPhase,
} from 'react-reconciler/src/ReactTreeTraversal';
import {
executeDirectDispatch,
hasDispatches,
executeDispatchesInOrderStopAtTrue,
getInstanceFromNode,
} from './EventPluginUtils';
import {accumulateDirectDispatches} from './EventPropagators';
import ResponderSyntheticEvent from './ResponderSyntheticEvent';
import ResponderTouchHistoryStore from './ResponderTouchHistoryStore';
import accumulate from './accumulate';
@ -36,6 +28,7 @@ import {
import getListener from './getListener';
import accumulateInto from './accumulateInto';
import forEachAccumulated from './forEachAccumulated';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
/**
* Instance of element that should respond to touch/move types of interactions,
@ -158,6 +151,91 @@ const eventTypes = {
// Start of inline: the below functions were inlined from
// EventPropagator.js, as they deviated from ReactDOM's newer
// implementations.
function getParent(inst) {
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (inst && inst.tag !== HostComponent);
if (inst) {
return inst;
}
return null;
}
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
export function getLowestCommonAncestor(instA, instB) {
let depthA = 0;
for (let tempA = instA; tempA; tempA = getParent(tempA)) {
depthA++;
}
let depthB = 0;
for (let tempB = instB; tempB; tempB = getParent(tempB)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
instA = getParent(instA);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
instB = getParent(instB);
depthB--;
}
// Walk in lockstep until we find a match.
let depth = depthA;
while (depth--) {
if (instA === instB || instA === instB.alternate) {
return instA;
}
instA = getParent(instA);
instB = getParent(instB);
}
return null;
}
/**
* Return if A is an ancestor of B.
*/
export function isAncestor(instA, instB) {
while (instB) {
if (instA === instB || instA === instB.alternate) {
return true;
}
instB = getParent(instB);
}
return false;
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
const registrationName =
event.dispatchConfig.phasedRegistrationNames[propagationPhase];
@ -180,10 +258,48 @@ function accumulateDirectionalDispatches(inst, phase, event) {
}
}
/**
* Accumulates without regard to direction, does not look for phased
* registration names. Same as `accumulateDirectDispatchesSingle` but without
* requiring that the `dispatchMarker` be the same as the dispatched ID.
*/
function accumulateDispatches(
inst: Object,
ignoredDirection: ?boolean,
event: Object,
): void {
if (inst && event && event.dispatchConfig.registrationName) {
const registrationName = event.dispatchConfig.registrationName;
const listener = getListener(inst, registrationName);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
}
/**
* Accumulates dispatches on an `SyntheticEvent`, but only for the
* `dispatchMarker`.
* @param {SyntheticEvent} event
*/
function accumulateDirectDispatchesSingle(event: Object) {
if (event && event.dispatchConfig.registrationName) {
accumulateDispatches(event._targetInst, null, event);
}
}
function accumulateDirectDispatches(events: ?(Array<Object> | Object)) {
forEachAccumulated(events, accumulateDirectDispatchesSingle);
}
function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
const targetInst = event._targetInst;
const parentInst = targetInst ? getParentInstance(targetInst) : null;
const parentInst = targetInst ? getParent(targetInst) : null;
traverseTwoPhase(parentInst, accumulateDirectionalDispatches, event);
}
}

View File

@ -1378,7 +1378,8 @@ describe('ResponderEventPlugin', () => {
// ResponderEventPlugin uses `getLowestCommonAncestor`
const React = require('react');
const ReactTestUtils = require('react-dom/test-utils');
const ReactTreeTraversal = require('react-reconciler/src/ReactTreeTraversal');
const getLowestCommonAncestor = require('legacy-events/ResponderEventPlugin')
.getLowestCommonAncestor;
const ReactDOMComponentTree = require('../../react-dom/src/client/ReactDOMComponentTree');
class ChildComponent extends React.Component {
@ -1451,7 +1452,7 @@ describe('ResponderEventPlugin', () => {
let i;
for (i = 0; i < ancestors.length; i++) {
const plan = ancestors[i];
const firstCommon = ReactTreeTraversal.getLowestCommonAncestor(
const firstCommon = getLowestCommonAncestor(
ReactDOMComponentTree.getInstanceFromNode(plan.one),
ReactDOMComponentTree.getInstanceFromNode(plan.two),
);

View File

@ -5,8 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
import {accumulateEnterLeaveDispatches} from 'legacy-events/EventPropagators';
import {
TOP_MOUSE_OUT,
TOP_MOUSE_OVER,
@ -23,6 +21,7 @@ import {
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
import {getNearestMountedFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
import accumulateEnterLeaveListeners from './accumulateEnterLeaveListeners';
const eventTypes = {
mouseEnter: {
@ -172,7 +171,7 @@ const EnterLeaveEventPlugin = {
enter.target = toNode;
enter.relatedTarget = fromNode;
accumulateEnterLeaveDispatches(leave, enter, from, to);
accumulateEnterLeaveListeners(leave, enter, from, to);
if (!enableModernEventSystem) {
// If we are not processing the first ancestor, then we

View File

@ -0,0 +1,139 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
import getListener from 'legacy-events/getListener';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
function getParent(inst: Fiber | null): Fiber | null {
if (inst === null) {
return null;
}
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (inst && inst.tag !== HostComponent);
if (inst) {
return inst;
}
return null;
}
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
function getLowestCommonAncestor(instA: Fiber, instB: Fiber): Fiber | null {
let nodeA = instA;
let nodeB = instB;
let depthA = 0;
for (let tempA = nodeA; tempA; tempA = getParent(tempA)) {
depthA++;
}
let depthB = 0;
for (let tempB = nodeB; tempB; tempB = getParent(tempB)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
nodeA = getParent(nodeA);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
nodeB = getParent(nodeB);
depthB--;
}
// Walk in lockstep until we find a match.
let depth = depthA;
while (depth--) {
if (nodeA === nodeB || (nodeB !== null && nodeA === nodeB.alternate)) {
return nodeA;
}
nodeA = getParent(nodeA);
nodeB = getParent(nodeB);
}
return null;
}
function accumulateEnterLeaveListenersForEvent(
event: ReactSyntheticEvent,
target: Fiber,
common: Fiber | null,
capture: boolean,
): void {
const registrationName = event.dispatchConfig.registrationName;
if (registrationName === undefined) {
return;
}
const dispatchListeners = [];
const dispatchInstances = [];
let node = target;
while (node !== null) {
if (node === common) {
break;
}
const alternate = node.alternate;
if (alternate !== null && alternate === common) {
break;
}
if (node.tag === HostComponent) {
if (capture) {
const captureListener = getListener(node, registrationName);
if (captureListener != null) {
// Capture listeners/instances should go at the start, so we
// unshift them to the start of the array.
dispatchListeners.unshift(captureListener);
dispatchInstances.unshift(node);
}
} else {
const bubbleListener = getListener(node, registrationName);
if (bubbleListener != null) {
// Bubble listeners/instances should go at the end, so we
// push them to the end of the array.
dispatchListeners.push(bubbleListener);
dispatchInstances.push(node);
}
}
}
node = node.return;
}
// To prevent allocation to the event unless we actually
// have listeners we check the length of one of the arrays.
if (dispatchListeners.length > 0) {
event._dispatchListeners = dispatchListeners;
event._dispatchInstances = dispatchInstances;
}
}
export default function accumulateEnterLeaveListeners(
leaveEvent: ReactSyntheticEvent,
enterEvent: ReactSyntheticEvent,
from: Fiber | null,
to: Fiber | null,
): void {
const common = from && to ? getLowestCommonAncestor(from, to) : null;
if (from !== null) {
accumulateEnterLeaveListenersForEvent(leaveEvent, from, common, false);
}
if (to !== null) {
accumulateEnterLeaveListenersForEvent(enterEvent, to, common, true);
}
}

View File

@ -23,7 +23,6 @@ import {PLUGIN_EVENT_SYSTEM} from 'legacy-events/EventSystemFlags';
import act from './ReactTestUtilsAct';
import forEachAccumulated from 'legacy-events/forEachAccumulated';
import accumulateInto from 'legacy-events/accumulateInto';
import {traverseTwoPhase} from 'react-reconciler/src/ReactTreeTraversal';
const {findDOMNode} = ReactDOM;
// Keep in sync with ReactDOMUnstableNativeDependencies.js
@ -390,6 +389,39 @@ function isInteractive(tag) {
);
}
function getParent(inst) {
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (inst && inst.tag !== HostComponent);
if (inst) {
return inst;
}
return null;
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function shouldPreventMouseEvent(name, type, props) {
switch (name) {
case 'onClick':

View File

@ -9,7 +9,6 @@
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
import {accumulateDirectDispatches} from 'legacy-events/EventPropagators';
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
import SyntheticEvent from 'legacy-events/SyntheticEvent';
import invariant from 'shared/invariant';
@ -18,8 +17,8 @@ import invariant from 'shared/invariant';
import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import accumulateInto from 'legacy-events/accumulateInto';
import getListener from 'legacy-events/getListener';
import {traverseTwoPhase} from 'react-reconciler/src/ReactTreeTraversal';
import forEachAccumulated from 'legacy-events/forEachAccumulated';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
const {
customBubblingEventTypes,
@ -51,6 +50,39 @@ function accumulateDirectionalDispatches(inst, phase, event) {
}
}
function getParent(inst) {
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (inst && inst.tag !== HostComponent);
if (inst) {
return inst;
}
return null;
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) {
const path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
@ -60,6 +92,45 @@ function accumulateTwoPhaseDispatchesSingle(event) {
function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
/**
* Accumulates without regard to direction, does not look for phased
* registration names. Same as `accumulateDirectDispatchesSingle` but without
* requiring that the `dispatchMarker` be the same as the dispatched ID.
*/
function accumulateDispatches(
inst: Object,
ignoredDirection: ?boolean,
event: Object,
): void {
if (inst && event && event.dispatchConfig.registrationName) {
const registrationName = event.dispatchConfig.registrationName;
const listener = getListener(inst, registrationName);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
}
/**
* Accumulates dispatches on an `SyntheticEvent`, but only for the
* `dispatchMarker`.
* @param {SyntheticEvent} event
*/
function accumulateDirectDispatchesSingle(event: Object) {
if (event && event.dispatchConfig.registrationName) {
accumulateDispatches(event._targetInst, null, event);
}
}
function accumulateDirectDispatches(events: ?(Array<Object> | Object)) {
forEachAccumulated(events, accumulateDirectDispatchesSingle);
}
// End of inline
type PropagationPhases = 'bubbled' | 'captured';

View File

@ -1,146 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HostComponent} from './ReactWorkTags';
function getParent(inst) {
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (inst && inst.tag !== HostComponent);
if (inst) {
return inst;
}
return null;
}
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
export function getLowestCommonAncestor(instA, instB) {
let depthA = 0;
for (let tempA = instA; tempA; tempA = getParent(tempA)) {
depthA++;
}
let depthB = 0;
for (let tempB = instB; tempB; tempB = getParent(tempB)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
instA = getParent(instA);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
instB = getParent(instB);
depthB--;
}
// Walk in lockstep until we find a match.
let depth = depthA;
while (depth--) {
if (instA === instB || instA === instB.alternate) {
return instA;
}
instA = getParent(instA);
instB = getParent(instB);
}
return null;
}
/**
* Return if A is an ancestor of B.
*/
export function isAncestor(instA, instB) {
while (instB) {
if (instA === instB || instA === instB.alternate) {
return true;
}
instB = getParent(instB);
}
return false;
}
/**
* Return the parent instance of the passed-in instance.
*/
export function getParentInstance(inst) {
return getParent(inst);
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
export function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
/**
* Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
* should would receive a `mouseEnter` or `mouseLeave` event.
*
* Does not invoke the callback on the nearest common ancestor because nothing
* "entered" or "left" that element.
*/
export function traverseEnterLeave(from, to, fn, argFrom, argTo) {
const common = from && to ? getLowestCommonAncestor(from, to) : null;
const pathFrom = [];
while (true) {
if (!from) {
break;
}
if (from === common) {
break;
}
const alternate = from.alternate;
if (alternate !== null && alternate === common) {
break;
}
pathFrom.push(from);
from = getParent(from);
}
const pathTo = [];
while (true) {
if (!to) {
break;
}
if (to === common) {
break;
}
const alternate = to.alternate;
if (alternate !== null && alternate === common) {
break;
}
pathTo.push(to);
to = getParent(to);
}
for (let i = 0; i < pathFrom.length; i++) {
fn(pathFrom[i], 'bubbled', argFrom);
}
for (let i = pathTo.length; i-- > 0; ) {
fn(pathTo[i], 'captured', argTo);
}
}