Skip to content

Commit

Permalink
Eagerly listen to all replayable events
Browse files Browse the repository at this point in the history
To minimize breakages in a minor, I only do this for the new root APIs
since replaying only matters there anyway. Only if hydrating.

For Flare, I have to attach all active listeners since the current
system has one DOM listener for each. In a follow up I plan on optimizing
that by only attaching one if there's at least one active listener
which would allow us to start with only passive and then upgrade.
  • Loading branch information
sebmarkbage committed Sep 13, 2019
1 parent 3a4f7e7 commit bdacb27
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,12 @@ describe('ReactDOMServerPartialHydration', () => {
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);

// We'll do one click before hydrating.
a.click();
// This should be delayed.
expect(onEvent).toHaveBeenCalledTimes(0);

Scheduler.unstable_flushAll();
jest.runAllTimers();

Expand All @@ -1879,7 +1885,7 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler.unstable_flushAll();
jest.runAllTimers();

expect(onEvent).toHaveBeenCalledTimes(1);
expect(onEvent).toHaveBeenCalledTimes(2);

document.body.removeChild(container);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,13 @@ describe('ReactDOMServerHydration', () => {
// Hydrate asynchronously.
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);

// We haven't started hydrating yet.
a.click();
// Clicking should not invoke the event yet because we haven't committed
// the hydration yet.
expect(clicks).toBe(0);

// Flush part way through the render.
if (__DEV__) {
// In DEV effects gets double invoked.
Expand All @@ -592,7 +599,8 @@ describe('ReactDOMServerHydration', () => {
expect(Scheduler).toFlushAndYield(['Sibling2', 'Button']);
}

expect(clicks).toBe(1);
// We should have picked up both events now.
expect(clicks).toBe(2);

expect(container.textContent).toBe('Sibling');

Expand Down
32 changes: 19 additions & 13 deletions packages/react-dom/src/client/ReactDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
} from './ReactDOMComponentTree';
import {restoreControlledState} from './ReactDOMComponent';
import {dispatchEvent} from '../events/ReactDOMEventListener';
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
COMMENT_NODE,
Expand Down Expand Up @@ -365,7 +366,7 @@ ReactWork.prototype._onCommit = function(): void {
}
};

function ReactSyncRoot(
function createRootImpl(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
Expand All @@ -375,22 +376,27 @@ function ReactSyncRoot(
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
this._internalRoot = root;
markContainerAsRoot(root.current, container);
if (hydrate && tag !== LegacyRoot) {
const doc =
container.nodeType === DOCUMENT_NODE
? container
: container.ownerDocument;
eagerlyTrapReplayableEvents(doc);
}
return root;
}

function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}

function ReactRoot(container: DOMContainer, options: void | RootOptions) {
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
const root = createContainer(
container,
ConcurrentRoot,
hydrate,
hydrationCallbacks,
);
this._internalRoot = root;
markContainerAsRoot(root.current, container);
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}

ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
Expand Down
6 changes: 5 additions & 1 deletion packages/react-dom/src/events/EnterLeaveEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ const EnterLeaveEventPlugin = {
const isOutEvent =
topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT;

if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0 && (nativeEvent.relatedTarget || nativeEvent.fromElement)) {
if (
isOverEvent &&
(eventSystemFlags & IS_REPLAYED) === 0 &&
(nativeEvent.relatedTarget || nativeEvent.fromElement)
) {
// If this is an over event with a target, then we've already dispatched
// the event in the out event of the other target. If this is replayed,
// then it's because we couldn't dispatch against this target previously
Expand Down
80 changes: 44 additions & 36 deletions packages/react-dom/src/events/ReactBrowserEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,43 +134,51 @@ export function listenTo(

for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
if (!listeningSet.has(dependency)) {
switch (dependency) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
break;
case TOP_FOCUS:
case TOP_BLUR:
trapCapturedEvent(TOP_FOCUS, mountAt);
trapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
listeningSet.add(TOP_BLUR);
listeningSet.add(TOP_FOCUS);
break;
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(dependency))) {
trapCapturedEvent(dependency, mountAt);
}
break;
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
// We listen to them on the target DOM elements.
// Some of them bubble so we don't want them to fire twice.
break;
default:
// By default, listen on the top level to all non-media events.
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt);
}
break;
}
listeningSet.add(dependency);
listenToTopLevel(dependency, mountAt, listeningSet);
}
}

export function listenToTopLevel(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element | Node,
listeningSet: Set<DOMTopLevelEventType | string>,
): void {
if (!listeningSet.has(topLevelType)) {
switch (topLevelType) {
case TOP_SCROLL:
trapCapturedEvent(TOP_SCROLL, mountAt);
break;
case TOP_FOCUS:
case TOP_BLUR:
trapCapturedEvent(TOP_FOCUS, mountAt);
trapCapturedEvent(TOP_BLUR, mountAt);
// We set the flag for a single dependency later in this function,
// but this ensures we mark both as attached rather than just one.
listeningSet.add(TOP_BLUR);
listeningSet.add(TOP_FOCUS);
break;
case TOP_CANCEL:
case TOP_CLOSE:
if (isEventSupported(getRawEventName(topLevelType))) {
trapCapturedEvent(topLevelType, mountAt);
}
break;
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
// We listen to them on the target DOM elements.
// Some of them bubble so we don't want them to fire twice.
break;
default:
// By default, listen on the top level to all non-media events.
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(topLevelType, mountAt);
}
break;
}
listeningSet.add(topLevelType);
}
}

Expand Down
86 changes: 85 additions & 1 deletion packages/react-dom/src/events/ReactDOMEventReplaying.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@ import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';

import {enableFlareAPI} from 'shared/ReactFeatureFlags';
import {
unstable_scheduleCallback as scheduleCallback,
unstable_NormalPriority as NormalPriority,
} from 'scheduler';
import {attemptToDispatchEvent} from './ReactDOMEventListener';
import {
attemptToDispatchEvent,
trapEventForResponderEventSystem,
} from './ReactDOMEventListener';
import {
getListeningSetForElement,
listenToTopLevel,
} from './ReactBrowserEventEmitter';
import {unsafeCastDOMTopLevelTypeToString} from 'legacy-events/TopLevelEventTypes';

// TODO: Upgrade this definition once we're on a newer version of Flow that
// has this definition built-in.
Expand Down Expand Up @@ -140,6 +149,81 @@ export function isReplayableDiscreteEvent(
return false;
}

function trapReplayableEvent(
topLevelType: DOMTopLevelEventType,
document: Document,
listeningSet: Set<DOMTopLevelEventType | string>,
) {
listenToTopLevel(topLevelType, document, listeningSet);
if (enableFlareAPI) {
// Trap events for the responder system.
const passiveEventKey =
unsafeCastDOMTopLevelTypeToString(topLevelType) + '_passive';
if (!listeningSet.has(passiveEventKey)) {
trapEventForResponderEventSystem(document, topLevelType, true);
listeningSet.add(passiveEventKey);
}
// TODO: This listens to all events as active which might have
// undesirable effects. It's also unnecessary to have both
// passive and active listeners. Instead, we could start with
// a passive and upgrade it to an active one if needed.
// For replaying purposes the active is never needed since we
// currently don't preventDefault.
const activeEventKey =
unsafeCastDOMTopLevelTypeToString(topLevelType) + '_active';
if (!listeningSet.has(activeEventKey)) {
trapEventForResponderEventSystem(document, topLevelType, false);
listeningSet.add(activeEventKey);
}
}
}

export function eagerlyTrapReplayableEvents(document: Document) {
const listeningSet = getListeningSetForElement(document);
// Discrete
trapReplayableEvent(TOP_MOUSE_DOWN, document, listeningSet);
trapReplayableEvent(TOP_MOUSE_UP, document, listeningSet);
trapReplayableEvent(TOP_TOUCH_CANCEL, document, listeningSet);
trapReplayableEvent(TOP_TOUCH_END, document, listeningSet);
trapReplayableEvent(TOP_TOUCH_START, document, listeningSet);
trapReplayableEvent(TOP_AUX_CLICK, document, listeningSet);
trapReplayableEvent(TOP_DOUBLE_CLICK, document, listeningSet);
trapReplayableEvent(TOP_POINTER_CANCEL, document, listeningSet);
trapReplayableEvent(TOP_POINTER_DOWN, document, listeningSet);
trapReplayableEvent(TOP_POINTER_UP, document, listeningSet);
trapReplayableEvent(TOP_DRAG_END, document, listeningSet);
trapReplayableEvent(TOP_DRAG_START, document, listeningSet);
trapReplayableEvent(TOP_DROP, document, listeningSet);
trapReplayableEvent(TOP_COMPOSITION_END, document, listeningSet);
trapReplayableEvent(TOP_COMPOSITION_START, document, listeningSet);
trapReplayableEvent(TOP_KEY_DOWN, document, listeningSet);
trapReplayableEvent(TOP_KEY_PRESS, document, listeningSet);
trapReplayableEvent(TOP_KEY_UP, document, listeningSet);
trapReplayableEvent(TOP_INPUT, document, listeningSet);
trapReplayableEvent(TOP_TEXT_INPUT, document, listeningSet);
trapReplayableEvent(TOP_CLOSE, document, listeningSet);
trapReplayableEvent(TOP_CANCEL, document, listeningSet);
trapReplayableEvent(TOP_COPY, document, listeningSet);
trapReplayableEvent(TOP_CUT, document, listeningSet);
trapReplayableEvent(TOP_PASTE, document, listeningSet);
trapReplayableEvent(TOP_CLICK, document, listeningSet);
trapReplayableEvent(TOP_CHANGE, document, listeningSet);
trapReplayableEvent(TOP_CONTEXT_MENU, document, listeningSet);
trapReplayableEvent(TOP_RESET, document, listeningSet);
trapReplayableEvent(TOP_SUBMIT, document, listeningSet);
// Continuous
trapReplayableEvent(TOP_FOCUS, document, listeningSet);
trapReplayableEvent(TOP_BLUR, document, listeningSet);
trapReplayableEvent(TOP_DRAG_ENTER, document, listeningSet);
trapReplayableEvent(TOP_DRAG_LEAVE, document, listeningSet);
trapReplayableEvent(TOP_MOUSE_OVER, document, listeningSet);
trapReplayableEvent(TOP_MOUSE_OUT, document, listeningSet);
trapReplayableEvent(TOP_POINTER_OVER, document, listeningSet);
trapReplayableEvent(TOP_POINTER_OUT, document, listeningSet);
trapReplayableEvent(TOP_GOT_POINTER_CAPTURE, document, listeningSet);
trapReplayableEvent(TOP_LOST_POINTER_CAPTURE, document, listeningSet);
}

function createQueuedReplayableEvent(
blockedOn: null | Container | SuspenseInstance,
topLevelType: DOMTopLevelEventType,
Expand Down

0 comments on commit bdacb27

Please sign in to comment.