Skip to content

Commit d658ee8

Browse files
authored
Merge pull request #8661 from acdlite/fiberunbatchedupdates
[Fiber] Introduce API to opt-out of batching
2 parents 0402106 + ad7bcdf commit d658ee8

File tree

9 files changed

+113
-120
lines changed

9 files changed

+113
-120
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1207,7 +1207,8 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js
12071207
* can opt-in to deferred/animation scheduling inside componentDidMount/Update
12081208
* performs Task work even after time runs out
12091209
* does not perform animation work after time runs out
1210-
* can force synchronous updates with syncUpdates, even inside batchedUpdates
1210+
* can opt-out of batching using unbatchedUpdates
1211+
* nested updates are always deferred, even inside unbatchedUpdates
12111212

12121213
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
12131214
* can update child nodes of a host instance

src/renderers/dom/fiber/ReactDOMFiber.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ function renderSubtreeIntoContainer(parentComponent : ?ReactComponent<any, any,
326326
}
327327
const newRoot = DOMRenderer.createContainer(container);
328328
root = container._reactRootContainer = newRoot;
329-
// Initial mount is always sync, even if we're in a batch.
330-
DOMRenderer.syncUpdates(() => {
329+
// Initial mount should not be batched.
330+
DOMRenderer.unbatchedUpdates(() => {
331331
DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);
332332
});
333333
} else {
@@ -354,8 +354,8 @@ var ReactDOM = {
354354
unmountComponentAtNode(container : DOMContainerElement) {
355355
warnAboutUnstableUse();
356356
if (container._reactRootContainer) {
357-
// Unmount is always sync, even if we're in a batch.
358-
return DOMRenderer.syncUpdates(() => {
357+
// Unmount should not be batched.
358+
return DOMRenderer.unbatchedUpdates(() => {
359359
return renderSubtreeIntoContainer(null, null, container, () => {
360360
container._reactRootContainer = null;
361361
});

src/renderers/noop/ReactNoop.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ var ReactNoop = {
264264

265265
batchedUpdates: NoopRenderer.batchedUpdates,
266266

267+
unbatchedUpdates: NoopRenderer.unbatchedUpdates,
268+
267269
syncUpdates: NoopRenderer.syncUpdates,
268270

269271
// Logs the current state of the tree.

src/renderers/shared/__tests__/ReactPerf-test.js

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ describe('ReactPerf', () => {
1616
var ReactDOM;
1717
var ReactPerf;
1818
var ReactTestUtils;
19-
var ReactDOMFeatureFlags;
2019
var emptyFunction;
2120

2221
var App;
@@ -39,7 +38,6 @@ describe('ReactPerf', () => {
3938
ReactDOM = require('ReactDOM');
4039
ReactPerf = require('ReactPerf');
4140
ReactTestUtils = require('ReactTestUtils');
42-
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
4341
emptyFunction = require('emptyFunction');
4442

4543
App = class extends React.Component {
@@ -647,10 +645,6 @@ describe('ReactPerf', () => {
647645
var container = document.createElement('div');
648646
var thrownErr = new Error('Muhaha!');
649647

650-
if (ReactDOMFeatureFlags.useFiber) {
651-
spyOn(console, 'error');
652-
}
653-
654648
class Evil extends React.Component {
655649
componentWillMount() {
656650
throw thrownErr;
@@ -685,15 +679,6 @@ describe('ReactPerf', () => {
685679
}
686680
ReactDOM.unmountComponentAtNode(container);
687681
ReactPerf.stop();
688-
689-
if (ReactDOMFeatureFlags.useFiber) {
690-
// A sync `render` inside cWM will print a warning. That should be the
691-
// only warning.
692-
expect(console.error.calls.count()).toEqual(1);
693-
expect(console.error.calls.argsFor(0)[0]).toMatch(
694-
/Render methods should be a pure function of props and state/
695-
);
696-
}
697682
});
698683

699684
it('should not print errant warnings if portal throws in componentDidMount()', () => {

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export type Reconciler<C, I, TI> = {
8282
/* eslint-disable no-undef */
8383
// FIXME: ESLint complains about type parameter
8484
batchedUpdates<A>(fn : () => A) : A,
85+
unbatchedUpdates<A>(fn : () => A) : A,
8586
syncUpdates<A>(fn : () => A) : A,
8687
deferredUpdates<A>(fn : () => A) : A,
8788
/* eslint-enable no-undef */
@@ -107,6 +108,7 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
107108
getPriorityContext,
108109
performWithPriority,
109110
batchedUpdates,
111+
unbatchedUpdates,
110112
syncUpdates,
111113
deferredUpdates,
112114
} = ReactFiberScheduler(config);
@@ -161,6 +163,8 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
161163

162164
batchedUpdates,
163165

166+
unbatchedUpdates,
167+
164168
syncUpdates,
165169

166170
deferredUpdates,

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 29 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ var {
6969
if (__DEV__) {
7070
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
7171
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
72-
var warning = require('warning');
7372
}
7473

7574
var timeHeuristicForUnitOfWork = 1;
@@ -111,8 +110,8 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
111110
// Keeps track of whether we're currently in a work loop.
112111
let isPerformingWork : boolean = false;
113112

114-
// Keeps track of whether sync updates should be downgraded to task updates.
115-
let shouldDeferSyncUpdates : boolean = false;
113+
// Keeps track of whether we should should batch sync updates.
114+
let isBatchingUpdates : boolean = false;
116115

117116
// The next work in progress fiber that we're currently working on.
118117
let nextUnitOfWork : ?Fiber = null;
@@ -675,31 +674,10 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
675674
}
676675

677676
function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
678-
// If performWork is called recursively, we need to save the previous state
679-
// of the scheduler so it can be restored before the function exits.
680-
// Recursion is only possible when using syncUpdates.
681-
const previousPriorityContext = priorityContext;
682-
const previousPriorityContextBeforeReconciliation = priorityContextBeforeReconciliation;
683-
const previousIsPerformingWork = isPerformingWork;
684-
const previousShouldDeferSyncUpdates = shouldDeferSyncUpdates;
685-
const previousNextEffect = nextEffect;
686-
const previousCommitPhaseBoundaries = commitPhaseBoundaries;
687-
const previousFirstUncaughtError = firstUncaughtError;
688-
const previousFatalError = fatalError;
689-
const previousIsCommitting = isCommitting;
690-
const previousIsUnmounting = isUnmounting;
691-
692-
priorityContext = NoWork;
693-
priorityContextBeforeReconciliation = NoWork;
677+
if (isPerformingWork) {
678+
throw new Error('performWork was called recursively.');
679+
}
694680
isPerformingWork = true;
695-
shouldDeferSyncUpdates = true;
696-
nextEffect = null;
697-
commitPhaseBoundaries = null;
698-
firstUncaughtError = null;
699-
fatalError = null;
700-
isCommitting = false;
701-
isUnmounting = false;
702-
703681
const isPerformingDeferredWork = Boolean(deadline);
704682
let deadlineHasExpired = false;
705683

@@ -805,17 +783,12 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
805783

806784
const errorToThrow = fatalError || firstUncaughtError;
807785

808-
// We're done performing work. Restore the previous state of the scheduler.
809-
priorityContext = previousPriorityContext;
810-
priorityContextBeforeReconciliation = previousPriorityContextBeforeReconciliation;
811-
isPerformingWork = previousIsPerformingWork;
812-
shouldDeferSyncUpdates = previousShouldDeferSyncUpdates;
813-
nextEffect = previousNextEffect;
814-
commitPhaseBoundaries = previousCommitPhaseBoundaries;
815-
firstUncaughtError = previousFirstUncaughtError;
816-
fatalError = previousFatalError;
817-
isCommitting = previousIsCommitting;
818-
isUnmounting = previousIsUnmounting;
786+
// We're done performing work. Time to clean up.
787+
isPerformingWork = false;
788+
fatalError = null;
789+
firstUncaughtError = null;
790+
capturedErrors = null;
791+
failedBoundaries = null;
819792

820793
// It's safe to throw any unhandled errors.
821794
if (errorToThrow) {
@@ -1030,20 +1003,6 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
10301003
}
10311004

10321005
function scheduleUpdate(fiber : Fiber, priorityLevel : PriorityLevel) {
1033-
// Detect if a synchronous update is made during render (or begin phase).
1034-
if (priorityLevel === SynchronousPriority && isPerformingWork && !isCommitting) {
1035-
if (__DEV__) {
1036-
warning(
1037-
false,
1038-
'Render methods should be a pure function of props and state; ' +
1039-
'triggering nested component updates from render is not allowed. ' +
1040-
'If necessary, trigger nested updates in componentDidUpdate.'
1041-
);
1042-
}
1043-
// Downgrade to Task priority to prevent an infinite loop.
1044-
priorityLevel = TaskPriority;
1045-
}
1046-
10471006
let node = fiber;
10481007
let shouldContinue = true;
10491008
while (node && shouldContinue) {
@@ -1098,8 +1057,9 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
10981057
}
10991058

11001059
function getPriorityContext() : PriorityLevel {
1101-
// If we're in a batch, downgrade sync priority to task priority
1102-
if (priorityContext === SynchronousPriority && shouldDeferSyncUpdates) {
1060+
// If we're in a batch, or if we're already performing work, downgrade sync
1061+
// priority to task priority
1062+
if (priorityContext === SynchronousPriority && (isPerformingWork || isBatchingUpdates)) {
11031063
return TaskPriority;
11041064
}
11051065
return priorityContext;
@@ -1120,30 +1080,37 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
11201080
}
11211081

11221082
function batchedUpdates<A, R>(fn : (a: A) => R, a : A) : R {
1123-
const previousShouldDeferSyncUpdates = shouldDeferSyncUpdates;
1124-
shouldDeferSyncUpdates = true;
1083+
const previousIsBatchingUpdates = isBatchingUpdates;
1084+
isBatchingUpdates = true;
11251085
try {
11261086
return fn(a);
11271087
} finally {
1128-
shouldDeferSyncUpdates = previousShouldDeferSyncUpdates;
1088+
isBatchingUpdates = previousIsBatchingUpdates;
11291089
// If we're not already inside a batch, we need to flush any task work
11301090
// that was created by the user-provided function.
1131-
if (!shouldDeferSyncUpdates) {
1091+
if (!isPerformingWork && !isBatchingUpdates) {
11321092
performWork(TaskPriority);
11331093
}
11341094
}
11351095
}
11361096

1097+
function unbatchedUpdates<A>(fn : () => A) : A {
1098+
const previousIsBatchingUpdates = isBatchingUpdates;
1099+
isBatchingUpdates = false;
1100+
try {
1101+
return fn();
1102+
} finally {
1103+
isBatchingUpdates = previousIsBatchingUpdates;
1104+
}
1105+
}
1106+
11371107
function syncUpdates<A>(fn : () => A) : A {
11381108
const previousPriorityContext = priorityContext;
1139-
const previousShouldDeferSyncUpdates = shouldDeferSyncUpdates;
11401109
priorityContext = SynchronousPriority;
1141-
shouldDeferSyncUpdates = false;
11421110
try {
11431111
return fn();
11441112
} finally {
11451113
priorityContext = previousPriorityContext;
1146-
shouldDeferSyncUpdates = previousShouldDeferSyncUpdates;
11471114
}
11481115
}
11491116

@@ -1162,6 +1129,7 @@ module.exports = function<T, P, I, TI, C, CX, CI>(config : HostConfig<T, P, I, T
11621129
getPriorityContext: getPriorityContext,
11631130
performWithPriority: performWithPriority,
11641131
batchedUpdates: batchedUpdates,
1132+
unbatchedUpdates: unbatchedUpdates,
11651133
syncUpdates: syncUpdates,
11661134
deferredUpdates: deferredUpdates,
11671135
};

src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,13 +341,70 @@ describe('ReactIncrementalScheduling', () => {
341341
expect(ReactNoop.getChildren()).toEqual([span(1)]);
342342
});
343343

344-
it('can force synchronous updates with syncUpdates, even inside batchedUpdates', done => {
345-
ReactNoop.batchedUpdates(() => {
346-
ReactNoop.syncUpdates(() => {
347-
ReactNoop.render(<span />);
348-
expect(ReactNoop.getChildren()).toEqual([span()]);
349-
done();
344+
it('can opt-out of batching using unbatchedUpdates', () => {
345+
// syncUpdates gives synchronous priority to updates
346+
ReactNoop.syncUpdates(() => {
347+
// batchedUpdates downgrades sync updates to task priority
348+
ReactNoop.batchedUpdates(() => {
349+
ReactNoop.render(<span prop={0} />);
350+
expect(ReactNoop.getChildren()).toEqual([]);
351+
// Should not have flushed yet because we're still batching
352+
353+
// unbatchedUpdates reverses the effect of batchedUpdates, so sync
354+
// updates are not batched
355+
ReactNoop.unbatchedUpdates(() => {
356+
ReactNoop.render(<span prop={1} />);
357+
expect(ReactNoop.getChildren()).toEqual([span(1)]);
358+
ReactNoop.render(<span prop={2} />);
359+
expect(ReactNoop.getChildren()).toEqual([span(2)]);
360+
});
361+
362+
ReactNoop.render(<span prop={3} />);
363+
expect(ReactNoop.getChildren()).toEqual([span(2)]);
350364
});
365+
// Remaining update is now flushed
366+
expect(ReactNoop.getChildren()).toEqual([span(3)]);
351367
});
352368
});
369+
370+
it('nested updates are always deferred, even inside unbatchedUpdates', () => {
371+
let instance;
372+
let ops = [];
373+
class Foo extends React.Component {
374+
state = { step: 0 };
375+
componentDidUpdate() {
376+
ops.push('componentDidUpdate: ' + this.state.step);
377+
if (this.state.step === 1) {
378+
ReactNoop.unbatchedUpdates(() => {
379+
// This is a nested state update, so it should not be
380+
// flushed synchronously, even though we wrapped it
381+
// in unbatchedUpdates.
382+
this.setState({ step: 2 });
383+
});
384+
expect(ReactNoop.getChildren()).toEqual([span(1)]);
385+
}
386+
}
387+
render() {
388+
ops.push('render: ' + this.state.step);
389+
instance = this;
390+
return <span prop={this.state.step} />;
391+
}
392+
}
393+
ReactNoop.render(<Foo />);
394+
ReactNoop.flush();
395+
expect(ReactNoop.getChildren()).toEqual([span(0)]);
396+
397+
ReactNoop.syncUpdates(() => {
398+
instance.setState({ step: 1 });
399+
expect(ReactNoop.getChildren()).toEqual([span(2)]);
400+
});
401+
402+
expect(ops).toEqual([
403+
'render: 0',
404+
'render: 1',
405+
'componentDidUpdate: 1',
406+
'render: 2',
407+
'componentDidUpdate: 2',
408+
]);
409+
});
353410
});

src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ describe('ReactComponentTreeHook', () => {
1818
var ReactInstanceMap;
1919
var ReactComponentTreeHook;
2020
var ReactComponentTreeTestUtils;
21-
var ReactDOMFeatureFlags;
2221

2322
beforeEach(() => {
2423
jest.resetModules();
@@ -29,7 +28,6 @@ describe('ReactComponentTreeHook', () => {
2928
ReactInstanceMap = require('ReactInstanceMap');
3029
ReactComponentTreeHook = require('ReactComponentTreeHook');
3130
ReactComponentTreeTestUtils = require('ReactComponentTreeTestUtils');
32-
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
3331
});
3432

3533
function assertTreeMatches(pairs) {
@@ -1843,9 +1841,6 @@ describe('ReactComponentTreeHook', () => {
18431841
// https://github.com/facebook/react/issues/7187
18441842
var el = document.createElement('div');
18451843
var portalEl = document.createElement('div');
1846-
if (ReactDOMFeatureFlags.useFiber) {
1847-
spyOn(console, 'error');
1848-
}
18491844
class Foo extends React.Component {
18501845
componentWillMount() {
18511846
ReactDOM.render(<div />, portalEl);
@@ -1855,14 +1850,6 @@ describe('ReactComponentTreeHook', () => {
18551850
}
18561851
}
18571852
ReactDOM.render(<Foo />, el);
1858-
if (ReactDOMFeatureFlags.useFiber) {
1859-
// A sync `render` inside cWM will print a warning. That should be the
1860-
// only warning.
1861-
expect(console.error.calls.count()).toEqual(1);
1862-
expect(console.error.calls.argsFor(0)[0]).toMatch(
1863-
/Render methods should be a pure function of props and state/
1864-
);
1865-
}
18661853
});
18671854

18681855
it('is created when calling renderToString during render', () => {

0 commit comments

Comments
 (0)