Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flare] Update interactiveUpdates flushing heuristics #15687

Merged
merged 14 commits into from May 21, 2019
76 changes: 49 additions & 27 deletions packages/events/ReactGenericBatching.js
Expand Up @@ -17,15 +17,33 @@ import {
// scheduled work and instead do synchronous work.

// Defaults
let _batchedUpdatesImpl = function(fn, bookkeeping) {
let batchedUpdatesImpl = function(fn, bookkeeping) {
return fn(bookkeeping);
};
let _interactiveUpdatesImpl = function(fn, a, b, c) {
let discreteUpdatesImpl = function(fn, a, b, c) {
return fn(a, b, c);
};
let _flushInteractiveUpdatesImpl = function() {};
let flushDiscreteUpdatesImpl = function() {};
let batchedEventUpdatesImpl = batchedUpdatesImpl;

let isBatching = false;

function batchedUpdatesFinally() {
// Here we wait until all updates have propagated, which is important
// when using controlled components within layers:
// https://github.com/facebook/react/issues/1698
// Then we restore state of any controlled component.
isBatching = false;
const controlledComponentsHavePendingUpdates = needsStateRestore();
if (controlledComponentsHavePendingUpdates) {
// If a controlled event was fired, we may need to restore the state of
// the DOM node back to the controlled value. This is necessary when React
// bails out of the update without touching the DOM.
flushDiscreteUpdatesImpl();
restoreStateIfNeeded();
}
}

export function batchedUpdates(fn, bookkeeping) {
if (isBatching) {
// If we are currently inside another batch, we need to wait until it
Expand All @@ -34,38 +52,42 @@ export function batchedUpdates(fn, bookkeeping) {
}
isBatching = true;
try {
return _batchedUpdatesImpl(fn, bookkeeping);
return batchedUpdatesImpl(fn, bookkeeping);
} finally {
batchedUpdatesFinally();
}
}

export function batchedEventUpdates(fn, bookkeeping) {
if (isBatching) {
// If we are currently inside another batch, we need to wait until it
// fully completes before restoring state.
return fn(bookkeeping);
}
isBatching = true;
try {
return batchedEventUpdatesImpl(fn, bookkeeping);
} finally {
// Here we wait until all updates have propagated, which is important
// when using controlled components within layers:
// https://github.com/facebook/react/issues/1698
// Then we restore state of any controlled component.
isBatching = false;
const controlledComponentsHavePendingUpdates = needsStateRestore();
if (controlledComponentsHavePendingUpdates) {
// If a controlled event was fired, we may need to restore the state of
// the DOM node back to the controlled value. This is necessary when React
// bails out of the update without touching the DOM.
_flushInteractiveUpdatesImpl();
restoreStateIfNeeded();
}
batchedUpdatesFinally();
}
}

export function interactiveUpdates(fn, a, b, c) {
return _interactiveUpdatesImpl(fn, a, b, c);
export function discreteUpdates(fn, a, b, c) {
return discreteUpdatesImpl(fn, a, b, c);
}

export function flushInteractiveUpdates() {
return _flushInteractiveUpdatesImpl();
export function flushDiscreteUpdates() {
return flushDiscreteUpdatesImpl();
}

export function setBatchingImplementation(
batchedUpdatesImpl,
interactiveUpdatesImpl,
flushInteractiveUpdatesImpl,
_batchedUpdatesImpl,
_discreteUpdatesImpl,
_flushDiscreteUpdatesImpl,
_batchedEventUpdatesImpl,
) {
_batchedUpdatesImpl = batchedUpdatesImpl;
_interactiveUpdatesImpl = interactiveUpdatesImpl;
_flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;
batchedUpdatesImpl = _batchedUpdatesImpl;
discreteUpdatesImpl = _discreteUpdatesImpl;
flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
batchedEventUpdatesImpl = _batchedEventUpdatesImpl;
}
12 changes: 11 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFiber-test.js
Expand Up @@ -1026,9 +1026,15 @@ describe('ReactDOMFiber', () => {

it('should not update event handlers until commit', () => {
let ops = [];
let eventErrors = [];
const handlerA = () => ops.push('A');
const handlerB = () => ops.push('B');

spyOnProd(console, 'error');
window.addEventListener('error', e => {
eventErrors.push(e.message);
});

class Example extends React.Component {
state = {flip: false, count: 0};
flip() {
Expand Down Expand Up @@ -1090,12 +1096,16 @@ describe('ReactDOMFiber', () => {

// Because the new click handler has not yet committed, we should still
// invoke B.
expect(ops).toEqual(['B']);
expect(ops).toEqual([]);
ops = [];

// Any click that happens after commit, should invoke A.
node.click();
expect(ops).toEqual(['A']);
expect(eventErrors[0]).toEqual(
'unstable_flushDiscreteUpdates: Cannot flush ' +
'updates when React is already rendering.',
);
});

it('should not crash encountering low-priority tree', () => {
Expand Down
19 changes: 14 additions & 5 deletions packages/react-dom/src/client/ReactDOM.js
Expand Up @@ -26,10 +26,11 @@ import {
flushRoot,
createContainer,
updateContainer,
batchedEventUpdates,
batchedUpdates,
unbatchedUpdates,
interactiveUpdates,
flushInteractiveUpdates,
discreteUpdates,
flushDiscreteUpdates,
flushSync,
flushControlled,
injectIntoDevTools,
Expand Down Expand Up @@ -481,8 +482,9 @@ function shouldHydrateDueToLegacyHeuristic(container) {

setBatchingImplementation(
batchedUpdates,
interactiveUpdates,
flushInteractiveUpdates,
discreteUpdates,
flushDiscreteUpdates,
batchedEventUpdates,
);

let warnedAboutHydrateAPI = false;
Expand Down Expand Up @@ -783,7 +785,14 @@ const ReactDOM: Object = {

unstable_batchedUpdates: batchedUpdates,

unstable_interactiveUpdates: interactiveUpdates,
// TODO remove this legacy method, unstable_discreteUpdates replaces it
unstable_interactiveUpdates: (fn, a, b, c) => {
flushDiscreteUpdates();
return discreteUpdates(fn, a, b, c);
},

unstable_discreteUpdates: discreteUpdates,
unstable_flushDiscreteUpdates: flushDiscreteUpdates,

flushSync: flushSync,

Expand Down
35 changes: 31 additions & 4 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Expand Up @@ -26,7 +26,11 @@ import type {
ReactResponderDispatchEventOptions,
} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
import {
batchedEventUpdates,
discreteUpdates,
flushDiscreteUpdates,
} from 'events/ReactGenericBatching';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import warning from 'shared/warning';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
Expand Down Expand Up @@ -587,11 +591,14 @@ export function processEventQueue(): void {
return;
}
if (discrete) {
interactiveUpdates(() => {
batchedUpdates(processEvents, events);
if (shouldflushDiscreteUpdates(currentTimeStamp)) {
flushDiscreteUpdates();
}
discreteUpdates(() => {
batchedEventUpdates(processEvents, events);
});
} else {
batchedUpdates(processEvents, events);
batchedEventUpdates(processEvents, events);
}
}

Expand Down Expand Up @@ -990,3 +997,23 @@ export function generateListeningKey(
const passiveKey = passive ? '_passive' : '_active';
return `${topLevelType}${passiveKey}`;
}

let lastDiscreteEventTimeStamp = 0;

export function shouldflushDiscreteUpdates(timeStamp: number): boolean {
// event.timeStamp isn't overly reliable due to inconsistencies in
// how different browsers have historically provided the time stamp.
// Some browsers provide high-resolution time stamps for all events,
// some provide low-resoltion time stamps for all events. FF < 52
// even mixes both time stamps together. Some browsers even report
// negative time stamps or time stamps that are 0 (iOS9) in some cases.
// Given we are only comparing two time stamps with equality (!==),
// we are safe from the resolution differences. If the time stamp is 0
// we bail-out of preventing the flush, which shouldn't have any
// effect on the desired output, other than we over-flush.
trueadm marked this conversation as resolved.
Show resolved Hide resolved
if (timeStamp === 0 || lastDiscreteEventTimeStamp !== timeStamp) {
lastDiscreteEventTimeStamp = timeStamp;
return true;
}
return false;
}
27 changes: 16 additions & 11 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Expand Up @@ -11,9 +11,16 @@ import type {AnyNativeEvent} from 'events/PluginModuleType';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';

import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
import {
batchedEventUpdates,
discreteUpdates,
flushDiscreteUpdates,
} from 'events/ReactGenericBatching';
import {runExtractedPluginEventsInBatch} from 'events/EventPluginHub';
import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem';
import {
dispatchEventForResponderEventSystem,
shouldflushDiscreteUpdates,
} from '../events/DOMEventResponderSystem';
import {isFiberMounted} from 'react-reconciler/reflection';
import {HostRoot} from 'shared/ReactWorkTags';
import {
Expand Down Expand Up @@ -188,7 +195,7 @@ export function trapEventForResponderEventSystem(
} else {
eventFlags |= IS_ACTIVE;
}
// Check if interactive and wrap in interactiveUpdates
// Check if interactive and wrap in discreteUpdates
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
if (passiveBrowserEventsSupported) {
addEventCaptureListenerWithPassiveFlag(
Expand All @@ -212,7 +219,7 @@ function trapEventForPluginEventSystem(
? dispatchInteractiveEvent
: dispatchEvent;
const rawEventName = getRawEventName(topLevelType);
// Check if interactive and wrap in interactiveUpdates
// Check if interactive and wrap in discreteUpdates
const listener = dispatch.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
if (capture) {
addEventCaptureListener(element, rawEventName, listener);
Expand All @@ -222,12 +229,10 @@ function trapEventForPluginEventSystem(
}

function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) {
interactiveUpdates(
dispatchEvent,
topLevelType,
eventSystemFlags,
nativeEvent,
);
if (!enableEventAPI || shouldflushDiscreteUpdates(nativeEvent.timeStamp)) {
flushDiscreteUpdates();
}
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);
}

function dispatchEventForPluginEventSystem(
Expand All @@ -245,7 +250,7 @@ function dispatchEventForPluginEventSystem(
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
batchedUpdates(handleTopLevel, bookKeeping);
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
Expand Down
19 changes: 14 additions & 5 deletions packages/react-dom/src/fire/ReactFire.js
Expand Up @@ -31,10 +31,11 @@ import {
flushRoot,
createContainer,
updateContainer,
batchedEventUpdates,
batchedUpdates,
unbatchedUpdates,
interactiveUpdates,
flushInteractiveUpdates,
discreteUpdates,
flushDiscreteUpdates,
flushSync,
flushControlled,
injectIntoDevTools,
Expand Down Expand Up @@ -487,8 +488,9 @@ function shouldHydrateDueToLegacyHeuristic(container) {

setBatchingImplementation(
batchedUpdates,
interactiveUpdates,
flushInteractiveUpdates,
discreteUpdates,
flushDiscreteUpdates,
batchedEventUpdates,
);

let warnedAboutHydrateAPI = false;
Expand Down Expand Up @@ -789,7 +791,14 @@ const ReactDOM: Object = {

unstable_batchedUpdates: batchedUpdates,

unstable_interactiveUpdates: interactiveUpdates,
// TODO remove this legacy method, unstable_discreteUpdates replaces it
unstable_interactiveUpdates: (fn, a, b, c) => {
flushDiscreteUpdates();
return discreteUpdates(fn, a, b, c);
},

unstable_discreteUpdates: discreteUpdates,
unstable_flushDiscreteUpdates: flushDiscreteUpdates,

flushSync: flushSync,

Expand Down