@samitouri / QOS-React-2 / commits / 3ffb5d0876

Deterministic updates (#10715)

* Deterministic updates High priority updates typically require less work to render than low priority ones. It's beneficial to flush those first, in their own batch, before working on more expensive low priority ones. We do this even if a high priority is scheduled after a low priority one. However, we don't want this reordering of updates to affect the terminal state. State should be deterministic: once all work has been flushed, the final state should be the same regardless of how they were scheduled. To get both properties, we store updates on the queue in insertion order instead of priority order (always append). Then, when processing the queue, we skip over updates with insufficient priority. Instead of removing updates from the queue right after processing them, we only remove them if there are no unprocessed updates before it in the list. This means that updates may be processed more than once. As a bonus, the new implementation is simpler and requires less code. * Fix ceiling function Mixed up the operators. * Remove addUpdate, addReplaceState, et al These functions don't really do anything. Simpler to use a single insertUpdateIntoFiber function. Also splits scheduleUpdate into two functions: - scheduleWork traverses a fiber's ancestor path and updates their expiration times. - scheduleUpdate inserts an update into a fiber's update queue, then calls scheduleWork. * Remove getExpirationTime The last remaining use for getExpirationTime was for top-level async updates. I moved that check to scheduleUpdate instead. * Move UpdateQueue insertions back to class module Moves UpdateQueue related functions out of the scheduler and back into the class component module. It's a bit awkward that now we need to pass around createUpdateExpirationForFiber, too. But we can still do without addUpdate, replaceUpdate, et al. * Store callbacks as an array of Updates Simpler this way. Also moves commitCallbacks back to UpdateQueue module. * beginUpdateQueue -> processUpdateQueue * Updates should never have an expiration of NoWork * Rename expiration related functions * Fix update queue Flow types Gets rid of an unneccessary null check

Andrew Clark committed Oct 13, 2017 at 17:21 UTC 3ffb5d087638374057b16dbe04c8663fe372f45f
16 files changed +323 -439
package.json
+1
@@ -60,6 +60,7 @@
60 "glob-stream": "^6.1.0",
61 "gzip-js": "~0.3.2",
62 "gzip-size": "^3.0.0",
63 + "jasmine-check": "^1.0.0-rc.0",
64 "jest": "20.1.0-delta.1",
65 "jest-config": "20.1.0-delta.1",
66 "jest-jasmine2": "20.1.0-delta.1",
scripts/jest/test-framework-setup.js
+2
@@ -68,4 +68,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
68 return expectation;
69 };
70 global.expectDev = expectDev;
71 +
72 + require('jasmine-check').install();
73 }
src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js
+2 -2
@@ -284,8 +284,8 @@ describe('ReactDOMFiberAsync', () => {
284
285 // Flush the async updates
286 jest.runAllTimers();
287 - expect(container.textContent).toEqual('BCAD');
288 - expect(ops).toEqual(['BC', 'BCAD']);
287 + expect(container.textContent).toEqual('ABCD');
288 + expect(ops).toEqual(['BC', 'ABCD']);
289 });
290 });
291 });
src/renderers/noop/ReactNoopEntry.js
+1 -1
@@ -405,7 +405,7 @@ var ReactNoop = {
405 logHostInstances(container.children, depth + 1);
406 }
407
408 - function logUpdateQueue(updateQueue: UpdateQueue, depth) {
408 + function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
409 log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
410 const firstUpdate = updateQueue.first;
411 if (!firstUpdate) {
src/renderers/shared/fiber/ReactFiber.js
+1 -1
@@ -109,7 +109,7 @@ export type Fiber = {|
109 memoizedProps: any, // The props used to create the output.
110
111 // A queue of state updates and callbacks.
112 - updateQueue: UpdateQueue | null,
112 + updateQueue: UpdateQueue<any> | null,
113
114 // The state used to create the output
115 memoizedState: any,
src/renderers/shared/fiber/ReactFiberBeginWork.js
+7 -8
@@ -24,7 +24,7 @@ var {
24 reconcileChildFibersInPlace,
25 cloneChildFibers,
26 } = require('ReactChildFiber');
27 -var {beginUpdateQueue} = require('ReactFiberUpdateQueue');
27 +var {processUpdateQueue} = require('ReactFiberUpdateQueue');
28 var ReactTypeOfWork = require('ReactTypeOfWork');
29 var {
30 getMaskedContext,
@@ -71,8 +71,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
71 config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
72 hostContext: HostContext<C, CX>,
73 hydrationContext: HydrationContext<C, CX>,
74 - scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
75 - getExpirationTime: (fiber: Fiber, forceAsync: boolean) => ExpirationTime,
74 + scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
75 + computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
76 ) {
77 const {
78 shouldSetTextContent,
@@ -95,8 +95,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
95 // resumeMountClassInstance,
96 updateClassInstance,
97 } = ReactFiberClassComponent(
98 - scheduleUpdate,
99 - getExpirationTime,
98 + scheduleWork,
99 + computeExpirationForFiber,
100 memoizeProps,
101 memoizeState,
102 );
@@ -323,12 +323,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
323 const updateQueue = workInProgress.updateQueue;
324 if (updateQueue !== null) {
325 const prevState = workInProgress.memoizedState;
326 - const state = beginUpdateQueue(
326 + const state = processUpdateQueue(
327 current,
328 workInProgress,
329 updateQueue,
330 null,
331 - prevState,
331 null,
332 renderExpirationTime,
333 );
@@ -720,7 +719,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
719 function memoizeState(workInProgress: Fiber, nextState: any) {
720 workInProgress.memoizedState = nextState;
721 // Don't reset the updateQueue, in case there are pending updates. Resetting
723 - // is handled by beginUpdateQueue.
722 + // is handled by processUpdateQueue.
723 }
724
725 function beginWork(
src/renderers/shared/fiber/ReactFiberClassComponent.js
+45 -22
@@ -25,10 +25,8 @@ var {
25 isContextConsumer,
26 } = require('ReactFiberContext');
27 var {
28 - addUpdate,
29 - addReplaceUpdate,
30 - addForceUpdate,
31 - beginUpdateQueue,
28 + insertUpdateIntoFiber,
29 + processUpdateQueue,
30 } = require('ReactFiberUpdateQueue');
31 var {hasContextChanged} = require('ReactFiberContext');
32 var {isMounted} = require('ReactFiberTreeReflection');
@@ -77,8 +75,8 @@ if (__DEV__) {
75 }
76
77 module.exports = function(
80 - scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
81 - getExpirationTime: (fiber: Fiber, forceAsync: boolean) => ExpirationTime,
78 + scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
79 + computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
80 memoizeProps: (workInProgress: Fiber, props: any) => void,
81 memoizeState: (workInProgress: Fiber, state: any) => void,
82 ) {
@@ -87,33 +85,60 @@ module.exports = function(
85 isMounted,
86 enqueueSetState(instance, partialState, callback) {
87 const fiber = ReactInstanceMap.get(instance);
90 - const expirationTime = getExpirationTime(fiber, false);
88 callback = callback === undefined ? null : callback;
89 if (__DEV__) {
90 warnOnInvalidCallback(callback, 'setState');
91 }
95 - addUpdate(fiber, partialState, callback, expirationTime);
96 - scheduleUpdate(fiber, expirationTime);
92 + const expirationTime = computeExpirationForFiber(fiber);
93 + const update = {
94 + expirationTime,
95 + partialState,
96 + callback,
97 + isReplace: false,
98 + isForced: false,
99 + nextCallback: null,
100 + next: null,
101 + };
102 + insertUpdateIntoFiber(fiber, update);
103 + scheduleWork(fiber, expirationTime);
104 },
105 enqueueReplaceState(instance, state, callback) {
106 const fiber = ReactInstanceMap.get(instance);
100 - const expirationTime = getExpirationTime(fiber, false);
107 callback = callback === undefined ? null : callback;
108 if (__DEV__) {
109 warnOnInvalidCallback(callback, 'replaceState');
110 }
105 - addReplaceUpdate(fiber, state, callback, expirationTime);
106 - scheduleUpdate(fiber, expirationTime);
111 + const expirationTime = computeExpirationForFiber(fiber);
112 + const update = {
113 + expirationTime,
114 + partialState: state,
115 + callback,
116 + isReplace: true,
117 + isForced: false,
118 + nextCallback: null,
119 + next: null,
120 + };
121 + insertUpdateIntoFiber(fiber, update);
122 + scheduleWork(fiber, expirationTime);
123 },
124 enqueueForceUpdate(instance, callback) {
125 const fiber = ReactInstanceMap.get(instance);
110 - const expirationTime = getExpirationTime(fiber, false);
126 callback = callback === undefined ? null : callback;
127 if (__DEV__) {
128 warnOnInvalidCallback(callback, 'forceUpdate');
129 }
115 - addForceUpdate(fiber, callback, expirationTime);
116 - scheduleUpdate(fiber, expirationTime);
130 + const expirationTime = computeExpirationForFiber(fiber);
131 + const update = {
132 + expirationTime,
133 + partialState: null,
134 + callback,
135 + isReplace: false,
136 + isForced: true,
137 + nextCallback: null,
138 + next: null,
139 + };
140 + insertUpdateIntoFiber(fiber, update);
141 + scheduleWork(fiber, expirationTime);
142 },
143 };
144
@@ -404,7 +429,7 @@ module.exports = function(
429 const unmaskedContext = getUnmaskedContext(workInProgress);
430
431 instance.props = props;
407 - instance.state = state;
432 + instance.state = workInProgress.memoizedState = state;
433 instance.refs = emptyObject;
434 instance.context = getMaskedContext(workInProgress, unmaskedContext);
435
@@ -423,12 +448,11 @@ module.exports = function(
448 // process them now.
449 const updateQueue = workInProgress.updateQueue;
450 if (updateQueue !== null) {
426 - instance.state = beginUpdateQueue(
451 + instance.state = processUpdateQueue(
452 current,
453 workInProgress,
454 updateQueue,
455 instance,
431 - state,
456 props,
457 renderExpirationTime,
458 );
@@ -481,7 +505,7 @@ module.exports = function(
505 // // Process the update queue before calling shouldComponentUpdate
506 // const updateQueue = workInProgress.updateQueue;
507 // if (updateQueue !== null) {
484 - // newState = beginUpdateQueue(
508 + // newState = processUpdateQueue(
509 // workInProgress,
510 // updateQueue,
511 // instance,
@@ -524,7 +548,7 @@ module.exports = function(
548 // // componentWillMount may have called setState. Process the update queue.
549 // const newUpdateQueue = workInProgress.updateQueue;
550 // if (newUpdateQueue !== null) {
527 - // newState = beginUpdateQueue(
551 + // newState = processUpdateQueue(
552 // workInProgress,
553 // newUpdateQueue,
554 // instance,
@@ -590,12 +614,11 @@ module.exports = function(
614 // TODO: Previous state can be null.
615 let newState;
616 if (workInProgress.updateQueue !== null) {
593 - newState = beginUpdateQueue(
617 + newState = processUpdateQueue(
618 current,
619 workInProgress,
620 workInProgress.updateQueue,
621 instance,
598 - oldState,
622 newProps,
623 renderExpirationTime,
624 );
src/renderers/shared/fiber/ReactFiberCommitWork.js
+8 -13
@@ -30,12 +30,7 @@ var {
30 clearCaughtError,
31 } = require('ReactErrorUtils');
32
33 -var {
34 - Placement,
35 - Update,
36 - Callback,
37 - ContentReset,
38 -} = require('ReactTypeOfSideEffect');
33 +var {Placement, Update, ContentReset} = require('ReactTypeOfSideEffect');
34
35 var invariant = require('fbjs/lib/invariant');
36
@@ -132,19 +127,19 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
127 }
128 }
129 }
135 - if (
136 - finishedWork.effectTag & Callback &&
137 - finishedWork.updateQueue !== null
138 - ) {
139 - commitCallbacks(finishedWork, finishedWork.updateQueue, instance);
130 + const updateQueue = finishedWork.updateQueue;
131 + if (updateQueue !== null) {
132 + commitCallbacks(updateQueue, instance);
133 }
134 return;
135 }
136 case HostRoot: {
137 const updateQueue = finishedWork.updateQueue;
138 if (updateQueue !== null) {
146 - const instance = finishedWork.child && finishedWork.child.stateNode;
147 - commitCallbacks(finishedWork, updateQueue, instance);
139 + const instance = finishedWork.child !== null
140 + ? finishedWork.child.stateNode
141 + : null;
142 + commitCallbacks(updateQueue, instance);
143 }
144 return;
145 }
src/renderers/shared/fiber/ReactFiberExpirationTime.js
+5 -12
@@ -34,27 +34,20 @@ function msToExpirationTime(ms: number): ExpirationTime {
34 exports.msToExpirationTime = msToExpirationTime;
35
36 function ceiling(num: number, precision: number): number {
37 - return (((((num * precision) | 0) + 1) / precision) | 0) + 1;
37 + return (((num / precision) | 0) + 1) * precision;
38 }
39
40 -function bucket(
40 +function computeExpirationBucket(
41 currentTime: ExpirationTime,
42 expirationInMs: number,
43 - precisionInMs: number,
43 + bucketSizeMs: number,
44 ): ExpirationTime {
45 return ceiling(
46 currentTime + expirationInMs / UNIT_SIZE,
47 - precisionInMs / UNIT_SIZE,
47 + bucketSizeMs / UNIT_SIZE,
48 );
49 }
50 -
51 -// Given the current clock time, returns an expiration time. We use rounding
52 -// to batch like updates together.
53 -function asyncExpirationTime(currentTime: ExpirationTime) {
54 - // Should complete within ~1000ms. 1200ms max.
55 - return bucket(currentTime, 1000, 200);
56 -}
57 -exports.asyncExpirationTime = asyncExpirationTime;
50 +exports.computeExpirationBucket = computeExpirationBucket;
51
52 // Given the current clock time and an expiration time, returns the
53 // relative expiration time. Possible values include NoWork, Sync, Task, and
src/renderers/shared/fiber/ReactFiberReconciler.js
+32 -18
@@ -15,9 +15,6 @@ import type {FiberRoot} from 'ReactFiberRoot';
15 import type {ReactNodeList} from 'ReactTypes';
16
17 var ReactFeatureFlags = require('ReactFeatureFlags');
18 -
19 -var {addTopLevelUpdate} = require('ReactFiberUpdateQueue');
20 -
18 var {
19 findCurrentUnmaskedContext,
20 isContextProvider,
@@ -27,6 +24,7 @@ var {createFiberRoot} = require('ReactFiberRoot');
24 var ReactFiberScheduler = require('ReactFiberScheduler');
25 var ReactInstanceMap = require('ReactInstanceMap');
26 var {HostComponent} = require('ReactTypeOfWork');
27 +var {insertUpdateIntoFiber} = require('ReactFiberUpdateQueue');
28 var emptyObject = require('fbjs/lib/emptyObject');
29
30 if (__DEV__) {
@@ -265,8 +263,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
263 var {getPublicInstance} = config;
264
265 var {
268 - scheduleUpdate,
269 - getExpirationTime,
266 + computeAsyncExpiration,
267 + computeExpirationForFiber,
268 + scheduleWork,
269 batchedUpdates,
270 unbatchedUpdates,
271 flushSync,
@@ -296,17 +295,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
295 }
296 }
297
299 - // Check if the top-level element is an async wrapper component. If so, treat
300 - // updates to the root as async. This is a bit weird but lets us avoid a separate
301 - // `renderAsync` API.
302 - const forceAsync =
303 - ReactFeatureFlags.enableAsyncSubtreeAPI &&
304 - element != null &&
305 - element.type != null &&
306 - element.type.prototype != null &&
307 - (element.type.prototype: any).unstable_isAsyncReactComponent === true;
308 - const expirationTime = getExpirationTime(current, forceAsync);
309 - const nextState = {element};
298 callback = callback === undefined ? null : callback;
299 if (__DEV__) {
300 warning(
@@ -316,8 +304,34 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
304 callback,
305 );
306 }
319 - addTopLevelUpdate(current, nextState, callback, expirationTime);
320 - scheduleUpdate(current, expirationTime);
307 +
308 + let expirationTime;
309 + // Check if the top-level element is an async wrapper component. If so,
310 + // treat updates to the root as async. This is a bit weird but lets us
311 + // avoid a separate `renderAsync` API.
312 + if (
313 + ReactFeatureFlags.enableAsyncSubtreeAPI &&
314 + element != null &&
315 + element.type != null &&
316 + element.type.prototype != null &&
317 + (element.type.prototype: any).unstable_isAsyncReactComponent === true
318 + ) {
319 + expirationTime = computeAsyncExpiration();
320 + } else {
321 + expirationTime = computeExpirationForFiber(current);
322 + }
323 +
324 + const update = {
325 + expirationTime,
326 + partialState: {element},
327 + callback,
328 + isReplace: false,
329 + isForced: false,
330 + nextCallback: null,
331 + next: null,
332 + };
333 + insertUpdateIntoFiber(current, update);
334 + scheduleWork(current, expirationTime);
335 }
336
337 return {
src/renderers/shared/fiber/ReactFiberScheduler.js
+58 -56
@@ -59,7 +59,7 @@ var {
59 Sync,
60 Never,
61 msToExpirationTime,
62 - asyncExpirationTime,
62 + computeExpirationBucket,
63 relativeExpirationTime,
64 } = require('ReactFiberExpirationTime');
65
@@ -165,8 +165,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
165 config,
166 hostContext,
167 hydrationContext,
168 - scheduleUpdate,
169 - getExpirationTime,
168 + scheduleWork,
169 + computeExpirationForFiber,
170 );
171 const {completeWork} = ReactFiberCompleteWork(
172 config,
@@ -1373,11 +1373,58 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1373 }
1374 }
1375
1376 - function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) {
1377 - return scheduleUpdateImpl(fiber, expirationTime, false);
1376 + function computeAsyncExpiration() {
1377 + // Given the current clock time, returns an expiration time. We use rounding
1378 + // to batch like updates together.
1379 + // Should complete within ~1000ms. 1200ms max.
1380 + const currentTime = recalculateCurrentTime();
1381 + const expirationMs = 1000;
1382 + const bucketSizeMs = 200;
1383 + return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
1384 }
1385
1380 - function scheduleUpdateImpl(
1386 + function computeExpirationForFiber(fiber: Fiber) {
1387 + let expirationTime;
1388 + if (expirationContext !== NoWork) {
1389 + // An explicit expiration context was set;
1390 + expirationTime = expirationContext;
1391 + } else if (isPerformingWork) {
1392 + if (isCommitting) {
1393 + // Updates that occur during the commit phase should have sync priority
1394 + // by default.
1395 + expirationTime = Sync;
1396 + } else {
1397 + // Updates during the render phase should expire at the same time as
1398 + // the work that is being rendered.
1399 + expirationTime = nextRenderExpirationTime;
1400 + }
1401 + } else {
1402 + // No explicit expiration context was set, and we're not currently
1403 + // performing work. Calculate a new expiration time.
1404 + if (useSyncScheduling && !(fiber.internalContextTag & AsyncUpdates)) {
1405 + // This is a sync update
1406 + expirationTime = Sync;
1407 + } else {
1408 + // This is an async update
1409 + expirationTime = computeAsyncExpiration();
1410 + }
1411 + }
1412 +
1413 + if (
1414 + expirationTime === Sync &&
1415 + (isBatchingUpdates || (isUnbatchingUpdates && isCommitting))
1416 + ) {
1417 + // If we're in a batch, downgrade sync to task.
1418 + expirationTime = Task;
1419 + }
1420 + return expirationTime;
1421 + }
1422 +
1423 + function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1424 + return scheduleWorkImpl(fiber, expirationTime, false);
1425 + }
1426 +
1427 + function scheduleWorkImpl(
1428 fiber: Fiber,
1429 expirationTime: ExpirationTime,
1430 isErrorRecovery: boolean,
@@ -1483,53 +1530,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1530 }
1531 }
1532
1486 - function getExpirationTime(
1487 - fiber: Fiber,
1488 - forceAsync: boolean,
1489 - ): ExpirationTime {
1490 - let expirationTime;
1491 - if (expirationContext !== NoWork) {
1492 - // An explicit expiration context was set;
1493 - expirationTime = expirationContext;
1494 - } else if (isPerformingWork) {
1495 - if (isCommitting) {
1496 - // Updates that occur during the commit phase should have task priority
1497 - // by default.
1498 - expirationTime = Sync;
1499 - } else {
1500 - // Updates during the render phase should expire at the same time as
1501 - // the work that is being rendered.
1502 - expirationTime = nextRenderExpirationTime;
1503 - }
1504 - } else {
1505 - // No explicit expiration context was set, and we're not currently
1506 - // performing work. Calculate a new expiration time.
1507 - if (
1508 - useSyncScheduling &&
1509 - !(fiber.internalContextTag & AsyncUpdates) &&
1510 - !forceAsync
1511 - ) {
1512 - // This is a sync update
1513 - expirationTime = Sync;
1514 - } else {
1515 - // This is an async update
1516 - const currentTime = recalculateCurrentTime();
1517 - expirationTime = asyncExpirationTime(currentTime);
1518 - }
1519 - }
1520 -
1521 - if (
1522 - expirationTime === Sync &&
1523 - (isBatchingUpdates || (isUnbatchingUpdates && isCommitting))
1524 - ) {
1525 - // If we're in a batch, or in the commit phase, downgrade sync to task
1526 - return Task;
1527 - }
1528 - return expirationTime;
1529 - }
1530 -
1533 function scheduleErrorRecovery(fiber: Fiber) {
1532 - scheduleUpdateImpl(fiber, Task, true);
1534 + scheduleWorkImpl(fiber, Task, true);
1535 }
1536
1537 function recalculateCurrentTime(): ExpirationTime {
@@ -1590,8 +1592,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1592
1593 function deferredUpdates<A>(fn: () => A): A {
1594 const previousExpirationContext = expirationContext;
1593 - const currentTime = recalculateCurrentTime();
1594 - expirationContext = asyncExpirationTime(currentTime);
1595 + expirationContext = computeAsyncExpiration();
1596 try {
1597 return fn();
1598 } finally {
@@ -1600,8 +1601,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1601 }
1602
1603 return {
1603 - scheduleUpdate: scheduleUpdate,
1604 - getExpirationTime: getExpirationTime,
1604 + computeAsyncExpiration: computeAsyncExpiration,
1605 + computeExpirationForFiber: computeExpirationForFiber,
1606 + scheduleWork: scheduleWork,
1607 batchedUpdates: batchedUpdates,
1608 unbatchedUpdates: unbatchedUpdates,
1609 flushSync: flushSync,
src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+129 -289
@@ -20,6 +20,7 @@ const {NoWork} = require('ReactFiberExpirationTime');
20 const {ClassComponent, HostRoot} = require('ReactTypeOfWork');
21
22 const invariant = require('fbjs/lib/invariant');
23 +
24 if (__DEV__) {
25 var warning = require('fbjs/lib/warning');
26 }
@@ -31,14 +32,13 @@ type PartialState<State, Props> =
32 // Callbacks are not validated until invocation
33 type Callback = mixed;
34
34 -export type Update = {
35 +export type Update<State> = {
36 expirationTime: ExpirationTime,
37 partialState: PartialState<any, any>,
38 callback: Callback | null,
39 isReplace: boolean,
40 isForced: boolean,
40 - isTopLevelUnmount: boolean,
41 - next: Update | null,
41 + next: Update<State> | null,
42 };
43
44 // Singly linked-list of updates. When an update is scheduled, it is added to
@@ -52,25 +52,31 @@ export type Update = {
52 // The work-in-progress queue is always a subset of the current queue.
53 //
54 // When the tree is committed, the work-in-progress becomes the current.
55 -export type UpdateQueue = {
56 - first: Update | null,
57 - last: Update | null,
55 +export type UpdateQueue<State> = {
56 + // A processed update is not removed from the queue if there are any
57 + // unprocessed updates that came before it. In that case, we need to keep
58 + // track of the base state, which represents the base state of the first
59 + // unprocessed update, which is the same as the first update in the list.
60 + baseState: State,
61 + // For the same reason, we keep track of the remaining expiration time.
62 + expirationTime: ExpirationTime,
63 + first: Update<State> | null,
64 + last: Update<State> | null,
65 + callbackList: Array<Update<State>> | null,
66 hasForceUpdate: boolean,
59 - callbackList: null | Array<Callback>,
67
68 // Dev only
69 isProcessing?: boolean,
70 };
71
65 -let _queue1;
66 -let _queue2;
67 -
68 -function createUpdateQueue(): UpdateQueue {
69 - const queue: UpdateQueue = {
72 +function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
73 + const queue: UpdateQueue<State> = {
74 + baseState,
75 + expirationTime: NoWork,
76 first: null,
77 last: null,
72 - hasForceUpdate: false,
78 callbackList: null,
79 + hasForceUpdate: false,
80 };
81 if (__DEV__) {
82 queue.isProcessing = false;
@@ -78,120 +84,50 @@ function createUpdateQueue(): UpdateQueue {
84 return queue;
85 }
86
81 -function cloneUpdate(update: Update): Update {
82 - return {
83 - expirationTime: update.expirationTime,
84 - partialState: update.partialState,
85 - callback: update.callback,
86 - isReplace: update.isReplace,
87 - isForced: update.isForced,
88 - isTopLevelUnmount: update.isTopLevelUnmount,
89 - next: null,
90 - };
91 -}
92 -
93 -function insertUpdateIntoQueue(
94 - queue: UpdateQueue,
95 - update: Update,
96 - insertAfter: Update | null,
97 - insertBefore: Update | null,
98 -) {
99 - if (insertAfter !== null) {
100 - insertAfter.next = update;
101 - } else {
102 - // This is the first item in the queue.
103 - update.next = queue.first;
104 - queue.first = update;
105 - }
106 -
107 - if (insertBefore !== null) {
108 - update.next = insertBefore;
87 +function insertUpdateIntoQueue<State>(
88 + queue: UpdateQueue<State>,
89 + update: Update<State>,
90 +): void {
91 + // Append the update to the end of the list.
92 + if (queue.last === null) {
93 + // Queue is empty
94 + queue.first = queue.last = update;
95 } else {
110 - // This is the last item in the queue.
96 + queue.last.next = update;
97 queue.last = update;
98 }
113 -}
114 -
115 -// Returns the update after which the incoming update should be inserted into
116 -// the queue, or null if it should be inserted at beginning.
117 -function findInsertionPosition(queue, update): Update | null {
118 - const expirationTime = update.expirationTime;
119 - let insertAfter = null;
120 - let insertBefore = null;
121 - if (queue.last !== null && queue.last.expirationTime <= expirationTime) {
122 - // Fast path for the common case where the update should be inserted at
123 - // the end of the queue.
124 - insertAfter = queue.last;
125 - } else {
126 - insertBefore = queue.first;
127 - while (
128 - insertBefore !== null &&
129 - insertBefore.expirationTime <= expirationTime
130 - ) {
131 - insertAfter = insertBefore;
132 - insertBefore = insertBefore.next;
133 - }
99 + if (
100 + queue.expirationTime === NoWork ||
101 + queue.expirationTime > update.expirationTime
102 + ) {
103 + queue.expirationTime = update.expirationTime;
104 }
135 - return insertAfter;
105 }
106 +exports.insertUpdateIntoQueue = insertUpdateIntoQueue;
107
138 -function ensureUpdateQueues(fiber: Fiber) {
108 +function insertUpdateIntoFiber<State>(
109 + fiber: Fiber,
110 + update: Update<State>,
111 +): void {
112 + // We'll have at least one and at most two distinct update queues.
113 const alternateFiber = fiber.alternate;
140 -
114 let queue1 = fiber.updateQueue;
115 if (queue1 === null) {
143 - queue1 = fiber.updateQueue = createUpdateQueue();
116 + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
117 }
118
119 let queue2;
120 if (alternateFiber !== null) {
121 queue2 = alternateFiber.updateQueue;
122 if (queue2 === null) {
150 - queue2 = alternateFiber.updateQueue = createUpdateQueue();
123 + queue2 = alternateFiber.updateQueue = createUpdateQueue(
124 + alternateFiber.memoizedState,
125 + );
126 }
127 } else {
128 queue2 = null;
129 }
155 -
156 - _queue1 = queue1;
157 - // Return null if there is no alternate queue, or if its queue is the same.
158 - _queue2 = queue2 !== queue1 ? queue2 : null;
159 -}
160 -
161 -// The work-in-progress queue is a subset of the current queue (if it exists).
162 -// We need to insert the incoming update into both lists. However, it's possible
163 -// that the correct position in one list will be different from the position in
164 -// the other. Consider the following case:
165 -//
166 -// Current: 3-5-6
167 -// Work-in-progress: 6
168 -//
169 -// Then we receive an update with priority 4 and insert it into each list:
170 -//
171 -// Current: 3-4-5-6
172 -// Work-in-progress: 4-6
173 -//
174 -// In the current queue, the new update's `next` pointer points to the update
175 -// with priority 5. But in the work-in-progress queue, the pointer points to the
176 -// update with priority 6. Because these two queues share the same persistent
177 -// data structure, this won't do. (This can only happen when the incoming update
178 -// has higher priority than all the updates in the work-in-progress queue.)
179 -//
180 -// To solve this, in the case where the incoming update needs to be inserted
181 -// into two different positions, we'll make a clone of the update and insert
182 -// each copy into a separate queue. This forks the list while maintaining a
183 -// persistent structure, because the update that is added to the work-in-progress
184 -// is always added to the front of the list.
185 -//
186 -// However, if incoming update is inserted into the same position of both lists,
187 -// we shouldn't make a copy.
188 -//
189 -// If the update is cloned, it returns the cloned update.
190 -function insertUpdate(fiber: Fiber, update: Update): Update | null {
191 - // We'll have at least one and at most two distinct update queues.
192 - ensureUpdateQueues(fiber);
193 - const queue1 = _queue1;
194 - const queue2 = _queue2;
130 + queue2 = queue2 !== queue1 ? queue2 : null;
131
132 // Warn if an update is scheduled from inside an updater function.
133 if (__DEV__) {
@@ -206,161 +142,40 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
142 }
143 }
144
209 - // Find the insertion position in the first queue.
210 - const insertAfter1 = findInsertionPosition(queue1, update);
211 - const insertBefore1 = insertAfter1 !== null
212 - ? insertAfter1.next
213 - : queue1.first;
214 -
145 + // If there's only one queue, add the update to that queue and exit.
146 if (queue2 === null) {
216 - // If there's no alternate queue, there's nothing else to do but insert.
217 - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
218 - return null;
147 + insertUpdateIntoQueue(queue1, update);
148 + return;
149 }
150
221 - // If there is an alternate queue, find the insertion position.
222 - const insertAfter2 = findInsertionPosition(queue2, update);
223 - const insertBefore2 = insertAfter2 !== null
224 - ? insertAfter2.next
225 - : queue2.first;
226 -
227 - // Now we can insert into the first queue. This must come after finding both
228 - // insertion positions because it mutates the list.
229 - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
230 -
231 - // See if the insertion positions are equal. Be careful to only compare
232 - // non-null values.
233 - if (
234 - (insertBefore1 === insertBefore2 && insertBefore1 !== null) ||
235 - (insertAfter1 === insertAfter2 && insertAfter1 !== null)
236 - ) {
237 - // The insertion positions are the same, so when we inserted into the first
238 - // queue, it also inserted into the alternate. All we need to do is update
239 - // the alternate queue's `first` and `last` pointers, in case they
240 - // have changed.
241 - if (insertAfter2 === null) {
242 - queue2.first = update;
243 - }
244 - if (insertBefore2 === null) {
245 - queue2.last = null;
246 - }
247 - return null;
248 - } else {
249 - // The insertion positions are different, so we need to clone the update and
250 - // insert the clone into the alternate queue.
251 - const update2 = cloneUpdate(update);
252 - insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
253 - return update2;
151 + // If either queue is empty, we need to add to both queues.
152 + if (queue1.last === null || queue2.last === null) {
153 + insertUpdateIntoQueue(queue1, update);
154 + insertUpdateIntoQueue(queue2, update);
155 + return;
156 }
255 -}
256 -
257 -function addUpdate(
258 - fiber: Fiber,
259 - partialState: PartialState<any, any> | null,
260 - callback: mixed,
261 - expirationTime: ExpirationTime,
262 -): void {
263 - const update = {
264 - expirationTime,
265 - partialState,
266 - callback,
267 - isReplace: false,
268 - isForced: false,
269 - isTopLevelUnmount: false,
270 - next: null,
271 - };
272 - insertUpdate(fiber, update);
273 -}
274 -exports.addUpdate = addUpdate;
157
276 -function addReplaceUpdate(
277 - fiber: Fiber,
278 - state: any | null,
279 - callback: Callback | null,
280 - expirationTime: ExpirationTime,
281 -): void {
282 - const update = {
283 - expirationTime,
284 - partialState: state,
285 - callback,
286 - isReplace: true,
287 - isForced: false,
288 - isTopLevelUnmount: false,
289 - next: null,
290 - };
291 - insertUpdate(fiber, update);
158 + // If both lists are not empty, the last update is the same for both lists
159 + // because of structural sharing. So, we should only append to one of
160 + // the lists.
161 + insertUpdateIntoQueue(queue1, update);
162 + // But we still need to update the `last` pointer of queue2.
163 + queue2.last = update;
164 }
293 -exports.addReplaceUpdate = addReplaceUpdate;
294 -
295 -function addForceUpdate(
296 - fiber: Fiber,
297 - callback: Callback | null,
298 - expirationTime: ExpirationTime,
299 -): void {
300 - const update = {
301 - expirationTime,
302 - partialState: null,
303 - callback,
304 - isReplace: false,
305 - isForced: true,
306 - isTopLevelUnmount: false,
307 - next: null,
308 - };
309 - insertUpdate(fiber, update);
310 -}
311 -exports.addForceUpdate = addForceUpdate;
165 +exports.insertUpdateIntoFiber = insertUpdateIntoFiber;
166
167 function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
314 - const updateQueue = fiber.updateQueue;
315 - if (updateQueue === null) {
168 + if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
169 return NoWork;
170 }
318 - if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
171 + const updateQueue = fiber.updateQueue;
172 + if (updateQueue === null) {
173 return NoWork;
174 }
321 - return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork;
175 + return updateQueue.expirationTime;
176 }
177 exports.getUpdateExpirationTime = getUpdateExpirationTime;
178
325 -function addTopLevelUpdate(
326 - fiber: Fiber,
327 - partialState: PartialState<any, any>,
328 - callback: Callback | null,
329 - expirationTime: ExpirationTime,
330 -): void {
331 - const isTopLevelUnmount = partialState.element === null;
332 -
333 - const update = {
334 - expirationTime,
335 - partialState,
336 - callback,
337 - isReplace: false,
338 - isForced: false,
339 - isTopLevelUnmount,
340 - next: null,
341 - };
342 - const update2 = insertUpdate(fiber, update);
343 -
344 - if (isTopLevelUnmount) {
345 - // TODO: Redesign the top-level mount/update/unmount API to avoid this
346 - // special case.
347 - const queue1 = _queue1;
348 - const queue2 = _queue2;
349 -
350 - // Drop all updates that are lower-priority, so that the tree is not
351 - // remounted. We need to do this for both queues.
352 - if (queue1 !== null && update.next !== null) {
353 - update.next = null;
354 - queue1.last = update;
355 - }
356 - if (queue2 !== null && update2 !== null && update2.next !== null) {
357 - update2.next = null;
358 - queue2.last = update;
359 - }
360 - }
361 -}
362 -exports.addTopLevelUpdate = addTopLevelUpdate;
363 -
179 function getStateFromUpdate(update, instance, prevState, props) {
180 const partialState = update.partialState;
181 if (typeof partialState === 'function') {
@@ -371,19 +186,20 @@ function getStateFromUpdate(update, instance, prevState, props) {
186 }
187 }
188
374 -function beginUpdateQueue(
189 +function processUpdateQueue<State>(
190 current: Fiber | null,
191 workInProgress: Fiber,
377 - queue: UpdateQueue,
192 + queue: UpdateQueue<State>,
193 instance: any,
379 - prevState: any,
194 props: any,
195 renderExpirationTime: ExpirationTime,
382 -): any {
196 +): State {
197 if (current !== null && current.updateQueue === queue) {
198 // We need to create a work-in-progress queue, by cloning the current queue.
199 const currentQueue = queue;
200 queue = workInProgress.updateQueue = {
201 + baseState: currentQueue.baseState,
202 + expirationTime: currentQueue.expirationTime,
203 first: currentQueue.first,
204 last: currentQueue.last,
205 // These fields are no longer valid because they were already committed.
@@ -399,24 +215,47 @@ function beginUpdateQueue(
215 queue.isProcessing = true;
216 }
217
402 - // Calculate these using the the existing values as a base.
403 - let callbackList = queue.callbackList;
404 - let hasForceUpdate = queue.hasForceUpdate;
218 + // Reset the remaining expiration time. If we skip over any updates, we'll
219 + // increase this accordingly.
220 + queue.expirationTime = NoWork;
221
406 - // Applies updates with matching priority to the previous state to create
407 - // a new state object.
408 - let state = prevState;
222 + let state = queue.baseState;
223 let dontMutatePrevState = true;
224 let update = queue.first;
411 - while (update !== null && update.expirationTime <= renderExpirationTime) {
412 - // Remove each update from the queue right before it is processed. That way
413 - // if setState is called from inside an updater function, the new update
414 - // will be inserted in the correct position.
415 - queue.first = update.next;
416 - if (queue.first === null) {
417 - queue.last = null;
225 + let didSkip = false;
226 + while (update !== null) {
227 + const updateExpirationTime = update.expirationTime;
228 + if (updateExpirationTime > renderExpirationTime) {
229 + // This update does not have sufficient priority. Skip it.
230 + const remainingExpirationTime = queue.expirationTime;
231 + if (
232 + remainingExpirationTime === NoWork ||
233 + remainingExpirationTime > updateExpirationTime
234 + ) {
235 + // Update the remaining expiration time.
236 + queue.expirationTime = updateExpirationTime;
237 + }
238 + if (!didSkip) {
239 + didSkip = true;
240 + queue.baseState = state;
241 + }
242 + // Continue to the next update.
243 + update = update.next;
244 + continue;
245 + }
246 +
247 + // This update does have sufficient priority.
248 +
249 + // If no previous updates were skipped, drop this update from the queue by
250 + // advancing the head of the list.
251 + if (!didSkip) {
252 + queue.first = update.next;
253 + if (queue.first === null) {
254 + queue.last = null;
255 + }
256 }
257
258 + // Process the update
259 let partialState;
260 if (update.isReplace) {
261 state = getStateFromUpdate(update, instance, state, props);
@@ -425,6 +264,7 @@ function beginUpdateQueue(
264 partialState = getStateFromUpdate(update, instance, state, props);
265 if (partialState) {
266 if (dontMutatePrevState) {
267 + // $FlowFixMe: Idk how to type this properly.
268 state = Object.assign({}, state, partialState);
269 } else {
270 state = Object.assign(state, partialState);
@@ -433,29 +273,31 @@ function beginUpdateQueue(
273 }
274 }
275 if (update.isForced) {
436 - hasForceUpdate = true;
276 + queue.hasForceUpdate = true;
277 }
438 - // Second condition ignores top-level unmount callbacks if they are not the
439 - // last update in the queue, since a subsequent update will cause a remount.
440 - if (
441 - update.callback !== null &&
442 - !(update.isTopLevelUnmount && update.next !== null)
443 - ) {
444 - callbackList = callbackList !== null ? callbackList : [];
445 - callbackList.push(update.callback);
446 - workInProgress.effectTag |= CallbackEffect;
278 + if (update.callback !== null) {
279 + // Append to list of callbacks.
280 + let callbackList = queue.callbackList;
281 + if (callbackList === null) {
282 + callbackList = queue.callbackList = [];
283 + }
284 + callbackList.push(update);
285 }
286 update = update.next;
287 }
288
451 - queue.callbackList = callbackList;
452 - queue.hasForceUpdate = hasForceUpdate;
453 -
454 - if (queue.first === null && callbackList === null && !hasForceUpdate) {
455 - // The queue is empty and there are no callbacks. We can reset it.
289 + if (queue.callbackList !== null) {
290 + workInProgress.effectTag |= CallbackEffect;
291 + } else if (queue.first === null && !queue.hasForceUpdate) {
292 + // The queue is empty. We can reset it.
293 workInProgress.updateQueue = null;
294 }
295
296 + if (!didSkip) {
297 + didSkip = true;
298 + queue.baseState = state;
299 + }
300 +
301 if (__DEV__) {
302 // No longer processing.
303 queue.isProcessing = false;
@@ -463,23 +305,21 @@ function beginUpdateQueue(
305
306 return state;
307 }
466 -exports.beginUpdateQueue = beginUpdateQueue;
308 +exports.processUpdateQueue = processUpdateQueue;
309
468 -function commitCallbacks(
469 - finishedWork: Fiber,
470 - queue: UpdateQueue,
471 - context: mixed,
472 -) {
310 +function commitCallbacks<State>(queue: UpdateQueue<State>, context: any) {
311 const callbackList = queue.callbackList;
312 if (callbackList === null) {
313 return;
314 }
477 -
315 // Set the list to null to make sure they don't get called more than once.
316 queue.callbackList = null;
480 -
317 for (let i = 0; i < callbackList.length; i++) {
482 - const callback = callbackList[i];
318 + const update = callbackList[i];
319 + const callback = update.callback;
320 + // This update might be processed again. Clear the callback so it's only
321 + // called once.
322 + update.callback = null;
323 invariant(
324 typeof callback === 'function',
325 'Invalid argument passed as callback. Expected a function. Instead ' +
src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js
+5 -3
@@ -57,11 +57,13 @@ describe('ReactIncrementalScheduling', () => {
57 ReactNoop.render(<span prop={4} />);
58 });
59 });
60 + // The sync updates flush first.
61 + expect(ReactNoop.getChildren()).toEqual([span(4)]);
62
61 - // The low pri update should be flushed last, even though it was scheduled
62 - // before the sync updates.
63 + // The terminal value should be the last update that was scheduled,
64 + // regardless of priority. In this case, that's the last sync update.
65 ReactNoop.flush();
64 - expect(ReactNoop.getChildren()).toEqual([span(5)]);
66 + expect(ReactNoop.getChildren()).toEqual([span(4)]);
67 });
68
69 it('schedules top-level updates with same priority in order of insertion', () => {
src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
+1
@@ -246,6 +246,7 @@ describe('ReactIncrementalTriangle', () => {
246 simulate(step(1), flush(3), toggle(18), step(0));
247 simulate(step(4), flush(52), expire(1476), flush(17), step(0));
248 simulate(interrupt(), toggle(10), step(2), expire(990), flush(46));
249 + simulate(interrupt(), step(6), step(7), toggle(6), interrupt());
250 });
251
252 it('fuzz tester', () => {
src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
+16 -14
@@ -154,19 +154,20 @@ describe('ReactIncrementalUpdates', () => {
154 expect(ReactNoop.getChildren()).toEqual([span('')]);
155
156 // Schedule some more updates at different priorities{
157 - instance.setState(createUpdate('f'));
157 + instance.setState(createUpdate('d'));
158 ReactNoop.flushSync(() => {
159 - instance.setState(createUpdate('d'));
159 instance.setState(createUpdate('e'));
160 + instance.setState(createUpdate('f'));
161 });
162 instance.setState(createUpdate('g'));
163
164 // The sync updates should have flushed, but not the async ones
165 - expect(ReactNoop.getChildren()).toEqual([span('de')]);
165 + expect(ReactNoop.getChildren()).toEqual([span('ef')]);
166
167 - // Now flush the remaining work. Updates a, b, and c should be processed
168 - // again, since they were interrupted last time.
169 - expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'f', 'g']);
167 + // Now flush the remaining work. Even though e and f were already processed,
168 + // they should be processed again, to ensure that the terminal state
169 + // is deterministic.
170 + expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
171 expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
172 });
173
@@ -202,23 +203,24 @@ describe('ReactIncrementalUpdates', () => {
203 expect(ReactNoop.getChildren()).toEqual([span('')]);
204
205 // Schedule some more updates at different priorities{
205 - instance.setState(createUpdate('f'));
206 + instance.setState(createUpdate('d'));
207 ReactNoop.flushSync(() => {
207 - instance.setState(createUpdate('d'));
208 + instance.setState(createUpdate('e'));
209 // No longer a public API, but we can test that it works internally by
210 // reaching into the updater.
210 - instance.updater.enqueueReplaceState(instance, createUpdate('e'));
211 + instance.updater.enqueueReplaceState(instance, createUpdate('f'));
212 });
213 instance.setState(createUpdate('g'));
214
215 // The sync updates should have flushed, but not the async ones. Update d
216 // was dropped and replaced by e.
216 - expect(ReactNoop.getChildren()).toEqual([span('e')]);
217 + expect(ReactNoop.getChildren()).toEqual([span('f')]);
218
218 - // Now flush the remaining work. Updates a, b, and c should be processed
219 - // again, since they were interrupted last time.
220 - expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'f', 'g']);
221 - expect(ReactNoop.getChildren()).toEqual([span('abcefg')]);
219 + // Now flush the remaining work. Even though e and f were already processed,
220 + // they should be processed again, to ensure that the terminal state
221 + // is deterministic.
222 + expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
223 + expect(ReactNoop.getChildren()).toEqual([span('fg')]);
224 });
225
226 it('passes accumulation of previous updates to replaceState updater function', () => {
yarn.lock
+10
@@ -2614,6 +2614,12 @@ istanbul-reports@^1.1.1:
2614 dependencies:
2615 handlebars "^4.0.3"
2616
2617 +jasmine-check@^1.0.0-rc.0:
2618 + version "1.0.0-rc.0"
2619 + resolved "https://registry.yarnpkg.com/jasmine-check/-/jasmine-check-1.0.0-rc.0.tgz#117728c150078ecf211986c5f164275b71e937a4"
2620 + dependencies:
2621 + testcheck "^1.0.0-rc"
2622 +
2623 jest-changed-files@20.1.0-delta.1:
2624 version "20.1.0-delta.1"
2625 resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.1.0-delta.1.tgz#912f8eff09c79b28fc7b66513f0ad505f1b378d5"
@@ -4316,6 +4322,10 @@ test-exclude@^4.0.3:
4322 read-pkg-up "^1.0.1"
4323 require-main-filename "^1.0.1"
4324
4325 +testcheck@^1.0.0-rc:
4326 + version "1.0.0-rc.2"
4327 + resolved "https://registry.yarnpkg.com/testcheck/-/testcheck-1.0.0-rc.2.tgz#11356a25b84575efe0b0857451e85b5fa74ee4e4"
4328 +
4329 text-table@~0.2.0:
4330 version "0.2.0"
4331 resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"