Skip to content

Commit ef4ac42

Browse files
authored
[Flare] Update interactiveUpdates flushing heuristics (#15687)
1 parent 6d4f85b commit ef4ac42

14 files changed

+343
-89
lines changed

Diff for: packages/events/ReactGenericBatching.js

+49-27
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,33 @@ import {
1717
// scheduled work and instead do synchronous work.
1818

1919
// Defaults
20-
let _batchedUpdatesImpl = function(fn, bookkeeping) {
20+
let batchedUpdatesImpl = function(fn, bookkeeping) {
2121
return fn(bookkeeping);
2222
};
23-
let _interactiveUpdatesImpl = function(fn, a, b, c) {
23+
let discreteUpdatesImpl = function(fn, a, b, c) {
2424
return fn(a, b, c);
2525
};
26-
let _flushInteractiveUpdatesImpl = function() {};
26+
let flushDiscreteUpdatesImpl = function() {};
27+
let batchedEventUpdatesImpl = batchedUpdatesImpl;
2728

2829
let isBatching = false;
30+
31+
function batchedUpdatesFinally() {
32+
// Here we wait until all updates have propagated, which is important
33+
// when using controlled components within layers:
34+
// https://github.com/facebook/react/issues/1698
35+
// Then we restore state of any controlled component.
36+
isBatching = false;
37+
const controlledComponentsHavePendingUpdates = needsStateRestore();
38+
if (controlledComponentsHavePendingUpdates) {
39+
// If a controlled event was fired, we may need to restore the state of
40+
// the DOM node back to the controlled value. This is necessary when React
41+
// bails out of the update without touching the DOM.
42+
flushDiscreteUpdatesImpl();
43+
restoreStateIfNeeded();
44+
}
45+
}
46+
2947
export function batchedUpdates(fn, bookkeeping) {
3048
if (isBatching) {
3149
// If we are currently inside another batch, we need to wait until it
@@ -34,38 +52,42 @@ export function batchedUpdates(fn, bookkeeping) {
3452
}
3553
isBatching = true;
3654
try {
37-
return _batchedUpdatesImpl(fn, bookkeeping);
55+
return batchedUpdatesImpl(fn, bookkeeping);
56+
} finally {
57+
batchedUpdatesFinally();
58+
}
59+
}
60+
61+
export function batchedEventUpdates(fn, bookkeeping) {
62+
if (isBatching) {
63+
// If we are currently inside another batch, we need to wait until it
64+
// fully completes before restoring state.
65+
return fn(bookkeeping);
66+
}
67+
isBatching = true;
68+
try {
69+
return batchedEventUpdatesImpl(fn, bookkeeping);
3870
} finally {
39-
// Here we wait until all updates have propagated, which is important
40-
// when using controlled components within layers:
41-
// https://github.com/facebook/react/issues/1698
42-
// Then we restore state of any controlled component.
43-
isBatching = false;
44-
const controlledComponentsHavePendingUpdates = needsStateRestore();
45-
if (controlledComponentsHavePendingUpdates) {
46-
// If a controlled event was fired, we may need to restore the state of
47-
// the DOM node back to the controlled value. This is necessary when React
48-
// bails out of the update without touching the DOM.
49-
_flushInteractiveUpdatesImpl();
50-
restoreStateIfNeeded();
51-
}
71+
batchedUpdatesFinally();
5272
}
5373
}
5474

55-
export function interactiveUpdates(fn, a, b, c) {
56-
return _interactiveUpdatesImpl(fn, a, b, c);
75+
export function discreteUpdates(fn, a, b, c) {
76+
return discreteUpdatesImpl(fn, a, b, c);
5777
}
5878

59-
export function flushInteractiveUpdates() {
60-
return _flushInteractiveUpdatesImpl();
79+
export function flushDiscreteUpdates() {
80+
return flushDiscreteUpdatesImpl();
6181
}
6282

6383
export function setBatchingImplementation(
64-
batchedUpdatesImpl,
65-
interactiveUpdatesImpl,
66-
flushInteractiveUpdatesImpl,
84+
_batchedUpdatesImpl,
85+
_discreteUpdatesImpl,
86+
_flushDiscreteUpdatesImpl,
87+
_batchedEventUpdatesImpl,
6788
) {
68-
_batchedUpdatesImpl = batchedUpdatesImpl;
69-
_interactiveUpdatesImpl = interactiveUpdatesImpl;
70-
_flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;
89+
batchedUpdatesImpl = _batchedUpdatesImpl;
90+
discreteUpdatesImpl = _discreteUpdatesImpl;
91+
flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
92+
batchedEventUpdatesImpl = _batchedEventUpdatesImpl;
7193
}

Diff for: packages/react-dom/src/__tests__/ReactDOMFiber-test.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -1026,9 +1026,15 @@ describe('ReactDOMFiber', () => {
10261026

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

1033+
spyOnProd(console, 'error');
1034+
window.addEventListener('error', e => {
1035+
eventErrors.push(e.message);
1036+
});
1037+
10321038
class Example extends React.Component {
10331039
state = {flip: false, count: 0};
10341040
flip() {
@@ -1090,12 +1096,16 @@ describe('ReactDOMFiber', () => {
10901096

10911097
// Because the new click handler has not yet committed, we should still
10921098
// invoke B.
1093-
expect(ops).toEqual(['B']);
1099+
expect(ops).toEqual([]);
10941100
ops = [];
10951101

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

11011111
it('should not crash encountering low-priority tree', () => {

Diff for: packages/react-dom/src/client/ReactDOM.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ import {
2626
flushRoot,
2727
createContainer,
2828
updateContainer,
29+
batchedEventUpdates,
2930
batchedUpdates,
3031
unbatchedUpdates,
31-
interactiveUpdates,
32-
flushInteractiveUpdates,
32+
discreteUpdates,
33+
flushDiscreteUpdates,
3334
flushSync,
3435
flushControlled,
3536
injectIntoDevTools,
@@ -481,8 +482,9 @@ function shouldHydrateDueToLegacyHeuristic(container) {
481482

482483
setBatchingImplementation(
483484
batchedUpdates,
484-
interactiveUpdates,
485-
flushInteractiveUpdates,
485+
discreteUpdates,
486+
flushDiscreteUpdates,
487+
batchedEventUpdates,
486488
);
487489

488490
let warnedAboutHydrateAPI = false;
@@ -783,7 +785,14 @@ const ReactDOM: Object = {
783785

784786
unstable_batchedUpdates: batchedUpdates,
785787

786-
unstable_interactiveUpdates: interactiveUpdates,
788+
// TODO remove this legacy method, unstable_discreteUpdates replaces it
789+
unstable_interactiveUpdates: (fn, a, b, c) => {
790+
flushDiscreteUpdates();
791+
return discreteUpdates(fn, a, b, c);
792+
},
793+
794+
unstable_discreteUpdates: discreteUpdates,
795+
unstable_flushDiscreteUpdates: flushDiscreteUpdates,
787796

788797
flushSync: flushSync,
789798

Diff for: packages/react-dom/src/events/DOMEventResponderSystem.js

+33-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import type {
2525
ReactResponderEvent,
2626
} from 'shared/ReactTypes';
2727
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
28-
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
28+
import {
29+
batchedEventUpdates,
30+
discreteUpdates,
31+
flushDiscreteUpdates,
32+
} from 'events/ReactGenericBatching';
2933
import type {Fiber} from 'react-reconciler/src/ReactFiber';
3034
import warning from 'shared/warning';
3135
import {enableEventAPI} from 'shared/ReactFeatureFlags';
@@ -601,11 +605,14 @@ export function processEventQueue(): void {
601605
return;
602606
}
603607
if (discrete) {
604-
interactiveUpdates(() => {
605-
batchedUpdates(processEvents, events);
608+
if (shouldflushDiscreteUpdates(currentTimeStamp)) {
609+
flushDiscreteUpdates();
610+
}
611+
discreteUpdates(() => {
612+
batchedEventUpdates(processEvents, events);
606613
});
607614
} else {
608-
batchedUpdates(processEvents, events);
615+
batchedEventUpdates(processEvents, events);
609616
}
610617
}
611618

@@ -1004,3 +1011,25 @@ export function generateListeningKey(
10041011
const passiveKey = passive ? '_passive' : '_active';
10051012
return `${topLevelType}${passiveKey}`;
10061013
}
1014+
1015+
let lastDiscreteEventTimeStamp = 0;
1016+
1017+
export function shouldflushDiscreteUpdates(timeStamp: number): boolean {
1018+
// event.timeStamp isn't overly reliable due to inconsistencies in
1019+
// how different browsers have historically provided the time stamp.
1020+
// Some browsers provide high-resolution time stamps for all events,
1021+
// some provide low-resoltion time stamps for all events. FF < 52
1022+
// even mixes both time stamps together. Some browsers even report
1023+
// negative time stamps or time stamps that are 0 (iOS9) in some cases.
1024+
// Given we are only comparing two time stamps with equality (!==),
1025+
// we are safe from the resolution differences. If the time stamp is 0
1026+
// we bail-out of preventing the flush, which can affect semantics,
1027+
// such as if an earlier flush removes or adds event listeners that
1028+
// are fired in the subsequent flush. However, this is the same
1029+
// behaviour as we had before this change, so the risks are low.
1030+
if (timeStamp === 0 || lastDiscreteEventTimeStamp !== timeStamp) {
1031+
lastDiscreteEventTimeStamp = timeStamp;
1032+
return true;
1033+
}
1034+
return false;
1035+
}

Diff for: packages/react-dom/src/events/ReactDOMEventListener.js

+16-11
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@ import type {AnyNativeEvent} from 'events/PluginModuleType';
1111
import type {Fiber} from 'react-reconciler/src/ReactFiber';
1212
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
1313

14-
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
14+
import {
15+
batchedEventUpdates,
16+
discreteUpdates,
17+
flushDiscreteUpdates,
18+
} from 'events/ReactGenericBatching';
1519
import {runExtractedPluginEventsInBatch} from 'events/EventPluginHub';
16-
import {dispatchEventForResponderEventSystem} from '../events/DOMEventResponderSystem';
20+
import {
21+
dispatchEventForResponderEventSystem,
22+
shouldflushDiscreteUpdates,
23+
} from '../events/DOMEventResponderSystem';
1724
import {isFiberMounted} from 'react-reconciler/reflection';
1825
import {HostRoot} from 'shared/ReactWorkTags';
1926
import {
@@ -188,7 +195,7 @@ export function trapEventForResponderEventSystem(
188195
} else {
189196
eventFlags |= IS_ACTIVE;
190197
}
191-
// Check if interactive and wrap in interactiveUpdates
198+
// Check if interactive and wrap in discreteUpdates
192199
const listener = dispatchEvent.bind(null, topLevelType, eventFlags);
193200
if (passiveBrowserEventsSupported) {
194201
addEventCaptureListenerWithPassiveFlag(
@@ -212,7 +219,7 @@ function trapEventForPluginEventSystem(
212219
? dispatchInteractiveEvent
213220
: dispatchEvent;
214221
const rawEventName = getRawEventName(topLevelType);
215-
// Check if interactive and wrap in interactiveUpdates
222+
// Check if interactive and wrap in discreteUpdates
216223
const listener = dispatch.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM);
217224
if (capture) {
218225
addEventCaptureListener(element, rawEventName, listener);
@@ -222,12 +229,10 @@ function trapEventForPluginEventSystem(
222229
}
223230

224231
function dispatchInteractiveEvent(topLevelType, eventSystemFlags, nativeEvent) {
225-
interactiveUpdates(
226-
dispatchEvent,
227-
topLevelType,
228-
eventSystemFlags,
229-
nativeEvent,
230-
);
232+
if (!enableEventAPI || shouldflushDiscreteUpdates(nativeEvent.timeStamp)) {
233+
flushDiscreteUpdates();
234+
}
235+
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent);
231236
}
232237

233238
function dispatchEventForPluginEventSystem(
@@ -245,7 +250,7 @@ function dispatchEventForPluginEventSystem(
245250
try {
246251
// Event queue being processed in the same cycle allows
247252
// `preventDefault`.
248-
batchedUpdates(handleTopLevel, bookKeeping);
253+
batchedEventUpdates(handleTopLevel, bookKeeping);
249254
} finally {
250255
releaseTopLevelCallbackBookKeeping(bookKeeping);
251256
}

Diff for: packages/react-dom/src/fire/ReactFire.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ import {
3131
flushRoot,
3232
createContainer,
3333
updateContainer,
34+
batchedEventUpdates,
3435
batchedUpdates,
3536
unbatchedUpdates,
36-
interactiveUpdates,
37-
flushInteractiveUpdates,
37+
discreteUpdates,
38+
flushDiscreteUpdates,
3839
flushSync,
3940
flushControlled,
4041
injectIntoDevTools,
@@ -487,8 +488,9 @@ function shouldHydrateDueToLegacyHeuristic(container) {
487488

488489
setBatchingImplementation(
489490
batchedUpdates,
490-
interactiveUpdates,
491-
flushInteractiveUpdates,
491+
discreteUpdates,
492+
flushDiscreteUpdates,
493+
batchedEventUpdates,
492494
);
493495

494496
let warnedAboutHydrateAPI = false;
@@ -789,7 +791,14 @@ const ReactDOM: Object = {
789791

790792
unstable_batchedUpdates: batchedUpdates,
791793

792-
unstable_interactiveUpdates: interactiveUpdates,
794+
// TODO remove this legacy method, unstable_discreteUpdates replaces it
795+
unstable_interactiveUpdates: (fn, a, b, c) => {
796+
flushDiscreteUpdates();
797+
return discreteUpdates(fn, a, b, c);
798+
},
799+
800+
unstable_discreteUpdates: discreteUpdates,
801+
unstable_flushDiscreteUpdates: flushDiscreteUpdates,
793802

794803
flushSync: flushSync,
795804

0 commit comments

Comments
 (0)