Skip to content

Commit 53e787b

Browse files
authored
Replace noop's fake Scheduler implementation with mock Scheduler build (#14969)
* Replace noop's fake Scheduler implementation with mock Scheduler build The noop renderer has its own mock implementation of the Scheduler interface, with the ability to partially render work in tests. Now that this functionality has been lifted into a proper mock Scheduler build, we can use that instead. Most of the existing noop tests were unaffected, but I did have to make some changes. The biggest one involved passive effects: previously, they were scheduled on a separate queue from the queue that handles rendering. After this change, both rendering and effects are scheduled in the Scheduler queue. I think this is a better approach because tests no longer have to worry about the difference; if you call `flushAll`, all the work is flushed, both rendering and effects. But for those few tests that do care to flush the rendering without the effects, that's still possible using the `yieldValue` API. Follow-up: Do the same for test renderer. * Fix import to scheduler/unstable_mock
1 parent 3ada82b commit 53e787b

14 files changed

+479
-470
lines changed

packages/react-noop-renderer/src/createReactNoop.js

+19-146
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
1818
import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue';
1919
import type {ReactNodeList} from 'shared/ReactTypes';
2020

21+
import * as Scheduler from 'scheduler/unstable_mock';
2122
import {createPortal} from 'shared/ReactPortal';
2223
import expect from 'expect';
2324
import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
@@ -51,9 +52,6 @@ if (__DEV__) {
5152
}
5253

5354
function createReactNoop(reconciler: Function, useMutation: boolean) {
54-
let scheduledCallback = null;
55-
let scheduledCallbackTimeout = -1;
56-
let scheduledPassiveCallback = null;
5755
let instanceCounter = 0;
5856
let hostDiffCounter = 0;
5957
let hostUpdateCounter = 0;
@@ -218,8 +216,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
218216
);
219217
}
220218

221-
let elapsedTimeInMs = 0;
222-
223219
const sharedHostConfig = {
224220
getRootHostContext() {
225221
return NO_CONTEXT;
@@ -308,66 +304,23 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
308304
return inst;
309305
},
310306

311-
scheduleDeferredCallback(callback, options) {
312-
if (scheduledCallback) {
313-
throw new Error(
314-
'Scheduling a callback twice is excessive. Instead, keep track of ' +
315-
'whether the callback has already been scheduled.',
316-
);
317-
}
318-
scheduledCallback = callback;
319-
if (
320-
typeof options === 'object' &&
321-
options !== null &&
322-
typeof options.timeout === 'number'
323-
) {
324-
const newTimeout = options.timeout;
325-
if (
326-
scheduledCallbackTimeout === -1 ||
327-
scheduledCallbackTimeout > newTimeout
328-
) {
329-
scheduledCallbackTimeout = elapsedTimeInMs + newTimeout;
330-
}
331-
}
332-
return 0;
333-
},
334-
335-
cancelDeferredCallback() {
336-
if (scheduledCallback === null) {
337-
throw new Error('No callback is scheduled.');
338-
}
339-
scheduledCallback = null;
340-
scheduledCallbackTimeout = -1;
341-
},
307+
scheduleDeferredCallback: Scheduler.unstable_scheduleCallback,
308+
cancelDeferredCallback: Scheduler.unstable_cancelCallback,
342309

343-
shouldYield,
310+
shouldYield: Scheduler.unstable_shouldYield,
344311

345312
scheduleTimeout: setTimeout,
346313
cancelTimeout: clearTimeout,
347314
noTimeout: -1,
348-
schedulePassiveEffects(callback) {
349-
if (scheduledCallback) {
350-
throw new Error(
351-
'Scheduling a callback twice is excessive. Instead, keep track of ' +
352-
'whether the callback has already been scheduled.',
353-
);
354-
}
355-
scheduledPassiveCallback = callback;
356-
},
357-
cancelPassiveEffects() {
358-
if (scheduledPassiveCallback === null) {
359-
throw new Error('No passive effects callback is scheduled.');
360-
}
361-
scheduledPassiveCallback = null;
362-
},
315+
316+
schedulePassiveEffects: Scheduler.unstable_scheduleCallback,
317+
cancelPassiveEffects: Scheduler.unstable_cancelCallback,
363318

364319
prepareForCommit(): void {},
365320

366321
resetAfterCommit(): void {},
367322

368-
now(): number {
369-
return elapsedTimeInMs;
370-
},
323+
now: Scheduler.unstable_now,
371324

372325
isPrimaryRenderer: true,
373326
supportsHydration: false,
@@ -534,71 +487,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
534487
const roots = new Map();
535488
const DEFAULT_ROOT_ID = '<default>';
536489

537-
let yieldedValues: Array<mixed> = [];
538-
let didStop: boolean = false;
539-
let expectedNumberOfYields: number = -1;
540-
541-
function shouldYield() {
542-
if (
543-
expectedNumberOfYields !== -1 &&
544-
yieldedValues.length >= expectedNumberOfYields &&
545-
(scheduledCallbackTimeout === -1 ||
546-
elapsedTimeInMs < scheduledCallbackTimeout)
547-
) {
548-
// We yielded at least as many values as expected. Stop rendering.
549-
didStop = true;
550-
return true;
551-
}
552-
// Keep rendering.
553-
return false;
554-
}
555-
556-
function flushAll(): Array<mixed> {
557-
yieldedValues = [];
558-
while (scheduledCallback !== null) {
559-
const cb = scheduledCallback;
560-
scheduledCallback = null;
561-
const didTimeout =
562-
scheduledCallbackTimeout !== -1 &&
563-
scheduledCallbackTimeout < elapsedTimeInMs;
564-
cb(didTimeout);
565-
}
566-
const values = yieldedValues;
567-
yieldedValues = [];
568-
return values;
569-
}
570-
571-
function flushNumberOfYields(count: number): Array<mixed> {
572-
expectedNumberOfYields = count;
573-
didStop = false;
574-
yieldedValues = [];
575-
try {
576-
while (scheduledCallback !== null && !didStop) {
577-
const cb = scheduledCallback;
578-
scheduledCallback = null;
579-
const didTimeout =
580-
scheduledCallbackTimeout !== -1 &&
581-
scheduledCallbackTimeout < elapsedTimeInMs;
582-
cb(didTimeout);
583-
}
584-
return yieldedValues;
585-
} finally {
586-
expectedNumberOfYields = -1;
587-
didStop = false;
588-
yieldedValues = [];
589-
}
590-
}
591-
592-
function yieldValue(value: mixed): void {
593-
yieldedValues.push(value);
594-
}
595-
596-
function clearYields(): Array<mixed> {
597-
const values = yieldedValues;
598-
yieldedValues = [];
599-
return values;
600-
}
601-
602490
function childToJSX(child, text) {
603491
if (text !== null) {
604492
return text;
@@ -653,6 +541,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
653541
}
654542

655543
const ReactNoop = {
544+
_Scheduler: Scheduler,
545+
656546
getChildren(rootID: string = DEFAULT_ROOT_ID) {
657547
const container = rootContainers.get(rootID);
658548
if (container) {
@@ -763,14 +653,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
763653
return NoopRenderer.findHostInstance(component);
764654
},
765655

766-
// TODO: Should only be used via a Jest plugin (like we do with the
767-
// test renderer).
768-
unstable_flushWithoutYielding: flushAll,
769-
unstable_flushNumberOfYields: flushNumberOfYields,
770-
unstable_clearYields: clearYields,
771-
772656
flushNextYield(): Array<mixed> {
773-
return flushNumberOfYields(1);
657+
Scheduler.unstable_flushNumberOfYields(1);
658+
return Scheduler.unstable_clearYields();
774659
},
775660

776661
flushWithHostCounters(
@@ -788,7 +673,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
788673
hostUpdateCounter = 0;
789674
hostCloneCounter = 0;
790675
try {
791-
flushAll();
676+
Scheduler.flushAll();
792677
return useMutation
793678
? {
794679
hostDiffCounter,
@@ -805,24 +690,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
805690
}
806691
},
807692

808-
expire(ms: number): Array<mixed> {
809-
ReactNoop.advanceTime(ms);
810-
return ReactNoop.flushExpired();
811-
},
812-
813-
advanceTime(ms: number): void {
814-
elapsedTimeInMs += ms;
815-
},
693+
expire: Scheduler.advanceTime,
816694

817695
flushExpired(): Array<mixed> {
818-
return flushNumberOfYields(0);
696+
return Scheduler.unstable_flushExpired();
819697
},
820698

821-
yield: yieldValue,
822-
823-
hasScheduledCallback() {
824-
return !!scheduledCallback;
825-
},
699+
yield: Scheduler.yieldValue,
826700

827701
batchedUpdates: NoopRenderer.batchedUpdates,
828702

@@ -870,9 +744,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
870744
},
871745

872746
flushSync(fn: () => mixed) {
873-
yieldedValues = [];
874747
NoopRenderer.flushSync(fn);
875-
return yieldedValues;
876748
},
877749

878750
flushPassiveEffects() {
@@ -997,12 +869,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
997869
_next: null,
998870
};
999871
root.firstBatch = batch;
1000-
const actual = flushAll();
872+
Scheduler.unstable_flushWithoutYielding();
873+
const actual = Scheduler.unstable_clearYields();
1001874
expect(actual).toEqual(expectedFlush);
1002875
return (expectedCommit: Array<mixed>) => {
1003876
batch._defer = false;
1004877
NoopRenderer.flushRoot(root, expiration);
1005-
expect(yieldedValues).toEqual(expectedCommit);
878+
expect(Scheduler.unstable_clearYields()).toEqual(expectedCommit);
1006879
};
1007880
},
1008881

packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js

+44-23
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
let React;
1313
let ReactFeatureFlags;
1414
let ReactNoop;
15+
let Scheduler;
1516

1617
describe('ReactExpiration', () => {
1718
beforeEach(() => {
@@ -20,6 +21,7 @@ describe('ReactExpiration', () => {
2021
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
2122
React = require('react');
2223
ReactNoop = require('react-noop-renderer');
24+
Scheduler = require('scheduler');
2325
});
2426

2527
function span(prop) {
@@ -60,19 +62,33 @@ describe('ReactExpiration', () => {
6062
}
6163
}
6264

65+
function interrupt() {
66+
ReactNoop.flushSync(() => {
67+
ReactNoop.renderToRootWithID(null, 'other-root');
68+
});
69+
}
70+
6371
// First, show what happens for updates in two separate events.
6472
// Schedule an update.
6573
ReactNoop.render(<Text text="A" />);
66-
// Advance the timer and flush any work that expired. Flushing the expired
67-
// work signals to the renderer that the event has ended.
68-
ReactNoop.advanceTime(2000);
74+
// Advance the timer.
75+
Scheduler.advanceTime(2000);
76+
// Partially flush the the first update, then interrupt it.
77+
expect(ReactNoop).toFlushAndYieldThrough(['A [render]']);
78+
interrupt();
79+
6980
// Don't advance time by enough to expire the first update.
70-
expect(ReactNoop.flushExpired()).toEqual([]);
81+
expect(Scheduler).toHaveYielded([]);
7182
expect(ReactNoop.getChildren()).toEqual([]);
83+
7284
// Schedule another update.
7385
ReactNoop.render(<Text text="B" />);
7486
// The updates should flush in separate batches, since sufficient time
7587
// passed in between them *and* they occurred in separate events.
88+
// Note: This isn't necessarily the ideal behavior. It might be better to
89+
// batch these two updates together. The fact that they aren't batched
90+
// is an implementation detail. The important part of this unit test is that
91+
// they are batched if it's possible that they happened in the same event.
7692
expect(ReactNoop).toFlushAndYield([
7793
'A [render]',
7894
'A [commit]',
@@ -84,10 +100,7 @@ describe('ReactExpiration', () => {
84100
// Now do the same thing again, except this time don't flush any work in
85101
// between the two updates.
86102
ReactNoop.render(<Text text="A" />);
87-
// Advance the timer, but don't flush the expired work. Because we still
88-
// haven't entered an idle callback, the scheduler must assume that we're
89-
// inside the same event.
90-
ReactNoop.advanceTime(2000);
103+
Scheduler.advanceTime(2000);
91104
expect(ReactNoop).toHaveYielded([]);
92105
expect(ReactNoop.getChildren()).toEqual([span('B')]);
93106
// Schedule another update.
@@ -114,19 +127,33 @@ describe('ReactExpiration', () => {
114127
}
115128
}
116129

130+
function interrupt() {
131+
ReactNoop.flushSync(() => {
132+
ReactNoop.renderToRootWithID(null, 'other-root');
133+
});
134+
}
135+
117136
// First, show what happens for updates in two separate events.
118137
// Schedule an update.
119138
ReactNoop.render(<Text text="A" />);
120-
// Advance the timer and flush any work that expired. Flushing the expired
121-
// work signals to the renderer that the event has ended.
122-
ReactNoop.advanceTime(2000);
139+
// Advance the timer.
140+
Scheduler.advanceTime(2000);
141+
// Partially flush the the first update, then interrupt it.
142+
expect(ReactNoop).toFlushAndYieldThrough(['A [render]']);
143+
interrupt();
144+
123145
// Don't advance time by enough to expire the first update.
124-
expect(ReactNoop.flushExpired()).toEqual([]);
146+
expect(Scheduler).toHaveYielded([]);
125147
expect(ReactNoop.getChildren()).toEqual([]);
148+
126149
// Schedule another update.
127150
ReactNoop.render(<Text text="B" />);
128151
// The updates should flush in separate batches, since sufficient time
129152
// passed in between them *and* they occurred in separate events.
153+
// Note: This isn't necessarily the ideal behavior. It might be better to
154+
// batch these two updates together. The fact that they aren't batched
155+
// is an implementation detail. The important part of this unit test is that
156+
// they are batched if it's possible that they happened in the same event.
130157
expect(ReactNoop).toFlushAndYield([
131158
'A [render]',
132159
'A [commit]',
@@ -138,21 +165,15 @@ describe('ReactExpiration', () => {
138165
// Now do the same thing again, except this time don't flush any work in
139166
// between the two updates.
140167
ReactNoop.render(<Text text="A" />);
141-
// Advance the timer, but don't flush the expired work. Because we still
142-
// haven't entered an idle callback, the scheduler must assume that we're
143-
// inside the same event.
144-
ReactNoop.advanceTime(2000);
168+
Scheduler.advanceTime(2000);
145169
expect(ReactNoop).toHaveYielded([]);
146170
expect(ReactNoop.getChildren()).toEqual([span('B')]);
147171

148-
// Perform some synchronous work. Again, the scheduler must assume we're
149-
// inside the same event.
150-
ReactNoop.flushSync(() => {
151-
ReactNoop.renderToRootWithID('1', 'second-root');
152-
});
172+
// Perform some synchronous work. The scheduler must assume we're inside
173+
// the same event.
174+
interrupt();
153175

154-
// Even though React flushed a sync update, it should not have updated the
155-
// current time. Schedule another update.
176+
// Schedule another update.
156177
ReactNoop.render(<Text text="B" />);
157178
// The updates should flush in the same batch, since as far as the scheduler
158179
// knows, they may have occurred inside the same event.

0 commit comments

Comments
 (0)