Skip to content

Commit df68585

Browse files
committed
Use generator-based API for ReactNoop.yield
1 parent 9cec000 commit df68585

File tree

2 files changed

+136
-44
lines changed

2 files changed

+136
-44
lines changed

src/renderers/noop/ReactNoop.js

+42-10
Original file line numberDiff line numberDiff line change
@@ -213,25 +213,36 @@ var roots = new Map();
213213
var DEFAULT_ROOT_ID = '<default>';
214214

215215
let yieldBeforeNextUnitOfWork = false;
216+
let yieldValue = null;
216217

217-
function flushUnitsOfWork(n) {
218-
yieldBeforeNextUnitOfWork = false;
219-
const cb = scheduledDeferredCallback;
220-
if (cb !== null) {
218+
function* flushUnitsOfWork(n: number): Generator<mixed, void, void> {
219+
var didStop = false;
220+
while (!didStop && scheduledDeferredCallback !== null) {
221+
var cb = scheduledDeferredCallback;
221222
scheduledDeferredCallback = null;
222-
let unitsRemaining = n;
223+
yieldBeforeNextUnitOfWork = false;
224+
yieldValue = null;
225+
var unitsRemaining = n;
226+
var didYield = false;
223227
cb({
224228
timeRemaining() {
225229
if (yieldBeforeNextUnitOfWork) {
226-
yieldBeforeNextUnitOfWork = false;
230+
didYield = true;
227231
return 0;
228232
}
229233
if (unitsRemaining-- > 0) {
230234
return 999;
231235
}
236+
didStop = true;
232237
return 0;
233238
},
234239
});
240+
241+
if (didYield) {
242+
const valueToYield = yieldValue;
243+
yieldValue = null;
244+
yield valueToYield;
245+
}
235246
}
236247
}
237248

@@ -302,23 +313,44 @@ var ReactNoop = {
302313
flushDeferredPri(timeout: number = Infinity) {
303314
// The legacy version of this function decremented the timeout before
304315
// returning the new time.
305-
// TODO: Convert tests to use flushUnitsOfWork instead.
316+
// TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead.
306317
const n = timeout / 5 - 1;
307-
flushUnitsOfWork(n);
318+
const iterator = flushUnitsOfWork(n);
319+
let value = iterator.next();
320+
while (!value.done) {
321+
value = iterator.next();
322+
}
323+
// Don't flush animation priority in this legacy function. Some tests may
324+
// still rely on this behavior.
308325
},
309326

310327
flush() {
311328
ReactNoop.flushAnimationPri();
312329
ReactNoop.flushDeferredPri();
313330
},
314331

332+
*flushAndYield(unitsOfWork: number = Infinity): Generator<mixed, void, void> {
333+
for (const value of flushUnitsOfWork(unitsOfWork)) {
334+
yield value;
335+
}
336+
ReactNoop.flushAnimationPri();
337+
},
338+
315339
flushUnitsOfWork(n: number) {
316-
flushUnitsOfWork(n);
340+
const iterator = flushUnitsOfWork(n);
341+
let value = iterator.next();
342+
while (!value.done) {
343+
value = iterator.next();
344+
}
345+
// TODO: We should always flush animation priority after flushing normal/low
346+
// priority. Move this to flushUnitsOfWork generator once tests
347+
// are converted.
317348
ReactNoop.flushAnimationPri();
318349
},
319350

320-
yieldBeforeNextUnitOfWork() {
351+
yield(value: mixed) {
321352
yieldBeforeNextUnitOfWork = true;
353+
yieldValue = value;
322354
},
323355

324356
performAnimationWork(fn: Function) {

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

+94-34
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var React;
1515
var ReactNoop;
1616
var ReactFeatureFlags;
1717

18-
describe('ReactConcurrency', () => {
18+
describe('ReactIncrementalTriangle', () => {
1919
beforeEach(() => {
2020
jest.resetModules();
2121
React = require('react');
@@ -33,7 +33,6 @@ describe('ReactConcurrency', () => {
3333
let triangles = [];
3434
let leafTriangles = [];
3535
let yieldAfterEachRender = false;
36-
let lastRenderedTriangle = null;
3736
class Triangle extends React.Component {
3837
constructor(props) {
3938
super();
@@ -64,9 +63,8 @@ describe('ReactConcurrency', () => {
6463
);
6564
}
6665
render() {
67-
lastRenderedTriangle = this;
6866
if (yieldAfterEachRender) {
69-
ReactNoop.yieldBeforeNextUnitOfWork();
67+
ReactNoop.yield(this);
7068
}
7169
const {counter, depth} = this.props;
7270
if (depth === 0) {
@@ -83,7 +81,7 @@ describe('ReactConcurrency', () => {
8381
}
8482
}
8583

86-
let app;
84+
let appInstance;
8785
class App extends React.Component {
8886
state = {counter: 0};
8987
interrupt() {
@@ -98,16 +96,32 @@ describe('ReactConcurrency', () => {
9896
return currentCounter;
9997
}
10098
render() {
101-
app = this;
99+
appInstance = this;
102100
return <Triangle counter={this.state.counter} depth={3} />;
103101
}
104102
}
105103

106104
const depth = 3;
107-
ReactNoop.render(<App depth={depth} />);
108-
ReactNoop.flush();
109-
// Check initial mount
110-
treeIsConsistent(0);
105+
106+
function reset(nextStep = 0) {
107+
triangles = [];
108+
leafTriangles = [];
109+
110+
const previousYieldAfterEachRender = yieldAfterEachRender;
111+
yieldAfterEachRender = false;
112+
ReactNoop.render([]);
113+
ReactNoop.flush();
114+
ReactNoop.render(<App depth={depth} />);
115+
ReactNoop.flush();
116+
// Check initial mount
117+
treeIsConsistent(nextStep);
118+
yieldAfterEachRender = previousYieldAfterEachRender;
119+
return appInstance;
120+
}
121+
122+
reset();
123+
const totalChildren = leafTriangles.length;
124+
const totalTriangles = triangles.length;
111125

112126
function treeIsConsistent(counter, activeTriangles = new Set()) {
113127
let activeIndices = [];
@@ -136,15 +150,9 @@ describe('ReactConcurrency', () => {
136150
}
137151
}
138152

139-
function renderNextTriangle() {
140-
yieldAfterEachRender = true;
141-
lastRenderedTriangle = null;
142-
ReactNoop.flush();
143-
yieldAfterEachRender = false;
144-
return lastRenderedTriangle;
145-
}
146-
147153
function step(nextCounter, ...configs) {
154+
const app = reset();
155+
148156
const currentCounter = app.setCounter(nextCounter);
149157

150158
const onKeyframes = new Map();
@@ -156,25 +164,28 @@ describe('ReactConcurrency', () => {
156164
throw new Error('targetIndex should be the index of a leaf triangle');
157165
}
158166

159-
const onTriangles = onKeyframes.get(targetIndex) || new Set();
160-
onTriangles.add(targetTriangle);
161-
onKeyframes.set(onKeyframe, onTriangles);
167+
if (onKeyframe >= 0 && onKeyframe < totalTriangles) {
168+
const onTriangles = onKeyframes.get(targetIndex) || new Set();
169+
onTriangles.add(targetTriangle);
170+
onKeyframes.set(onKeyframe, onTriangles);
171+
}
162172

163-
const offTriangles = offKeyframes.get(targetIndex) || new Set();
164-
offTriangles.add(targetTriangle);
165-
offKeyframes.set(offKeyframe, offTriangles);
173+
if (offKeyframe >= 0 && offKeyframe < totalTriangles) {
174+
const offTriangles = offKeyframes.get(targetIndex) || new Set();
175+
offTriangles.add(targetTriangle);
176+
offKeyframes.set(offKeyframe, offTriangles);
177+
}
166178
}
167179

168180
const activeTriangles = new Set();
169181

182+
yieldAfterEachRender = true;
170183
let i = 0;
171-
let renderedTriangle = renderNextTriangle();
172-
while (renderedTriangle !== null) {
184+
for (var renderedTriangle of ReactNoop.flushAndYield()) {
173185
if (i++ > 999) {
174186
throw new Error('Infinite loop');
175187
}
176188
treeIsConsistent(currentCounter, activeTriangles);
177-
178189
var onTriangles = onKeyframes.get(renderedTriangle.index);
179190
if (onTriangles) {
180191
onTriangles.forEach(targetTriangle => {
@@ -184,6 +195,7 @@ describe('ReactConcurrency', () => {
184195
activeTriangles.add(targetTriangle);
185196
onTriangles.delete(targetTriangle);
186197
});
198+
onKeyframes.delete(renderedTriangle.index);
187199
}
188200

189201
var offTriangles = offKeyframes.get(renderedTriangle.index);
@@ -195,19 +207,24 @@ describe('ReactConcurrency', () => {
195207
activeTriangles.delete(targetTriangle);
196208
offTriangles.delete(targetTriangle);
197209
});
210+
offKeyframes.delete(renderedTriangle.index);
198211
}
199212

200213
app.interrupt();
201214
ReactNoop.flushAnimationPri();
202215
treeIsConsistent(currentCounter, activeTriangles);
216+
}
217+
yieldAfterEachRender = false;
203218

204-
renderedTriangle = renderNextTriangle();
219+
if (onKeyframes.size !== 0 || offKeyframes.size !== 0) {
220+
onKeyframes.forEach(k => console.log(k.size));
221+
throw new Error('Some keyframes were not fired.');
205222
}
206223

207224
treeIsConsistent(nextCounter, activeTriangles);
208225
}
209226

210-
return {step};
227+
return {step, totalChildren, totalTriangles};
211228
}
212229

213230
it('works', () => {
@@ -224,12 +241,55 @@ describe('ReactConcurrency', () => {
224241
// hi-pri update to "deactivate" that same node.
225242
step(2, [5, 5, 10]);
226243
step(3, [22, 20, 22]);
227-
step(4, [13, 10, 30]);
228-
step(5, [7, 35, 38]);
229-
step(6, [17, 8, 14]);
230244

231245
// Simulate multiple hover effects in the same step.
232-
step(7, [3, 4, 21], [17, 8, 14], [19, 7, 12]);
233-
step(8, [3, 4, 39], [17, 8, 14], [19, 7, 12]);
246+
step(4, [3, 4, 21], [17, 8, 14], [19, 7, 12]);
247+
step(5, [3, 4, 39], [17, 8, 14], [19, 7, 12]);
248+
249+
// The following tests are test cases that at one point or another failed.
250+
// Don't touch them (unless there's a good reason).
251+
step(6, [23, 11, 6]);
252+
step(7, [7, 4, 3], [4, 14, 6]);
253+
});
254+
255+
it('fuzz tester', () => {
256+
// This test is not deterministic because the inputs are randomized. It runs
257+
// a limited number of tests on every run. If it fails, it will output the
258+
// inputs that led to the failure. Add the failing case to the test above
259+
// to prevent future regressions.
260+
const limit = 100;
261+
262+
const {step, totalChildren, totalTriangles} = TriangleTester();
263+
264+
function randomInteger(min, max) {
265+
min = Math.ceil(min);
266+
max = Math.floor(max);
267+
return Math.floor(Math.random() * (max - min)) + min;
268+
}
269+
270+
function randomConfig() {
271+
const targetIndex = randomInteger(0, totalChildren);
272+
const onKeyframe = randomInteger(0, totalTriangles);
273+
// Allow for end keyframe to happen after entire tree has flushed
274+
const offKeyframe = randomInteger(0, totalTriangles * 2);
275+
return [targetIndex, onKeyframe, offKeyframe];
276+
}
277+
278+
for (let nextStep = 1; nextStep <= limit; nextStep++) {
279+
const configs = [randomConfig(), randomConfig(), randomConfig()];
280+
try {
281+
step(nextStep, ...configs);
282+
} catch (error) {
283+
console.error(error);
284+
const configStr = configs
285+
.map(config => `[${config.join(', ')}]`)
286+
.join(', ');
287+
throw new Error(
288+
`Triangle fuzz tester failure. Add this the ReactIncrementalTriangle-test suite and fix it:
289+
step(n, ${configStr})
290+
`,
291+
);
292+
}
293+
}
234294
});
235295
});

0 commit comments

Comments
 (0)