mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
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
This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
"glob-stream": "^6.1.0",
|
||||
"gzip-js": "~0.3.2",
|
||||
"gzip-size": "^3.0.0",
|
||||
"jasmine-check": "^1.0.0-rc.0",
|
||||
"jest": "20.1.0-delta.1",
|
||||
"jest-config": "20.1.0-delta.1",
|
||||
"jest-jasmine2": "20.1.0-delta.1",
|
||||
|
||||
@@ -68,4 +68,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
|
||||
return expectation;
|
||||
};
|
||||
global.expectDev = expectDev;
|
||||
|
||||
require('jasmine-check').install();
|
||||
}
|
||||
|
||||
@@ -284,8 +284,8 @@ describe('ReactDOMFiberAsync', () => {
|
||||
|
||||
// Flush the async updates
|
||||
jest.runAllTimers();
|
||||
expect(container.textContent).toEqual('BCAD');
|
||||
expect(ops).toEqual(['BC', 'BCAD']);
|
||||
expect(container.textContent).toEqual('ABCD');
|
||||
expect(ops).toEqual(['BC', 'ABCD']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -405,7 +405,7 @@ var ReactNoop = {
|
||||
logHostInstances(container.children, depth + 1);
|
||||
}
|
||||
|
||||
function logUpdateQueue(updateQueue: UpdateQueue, depth) {
|
||||
function logUpdateQueue(updateQueue: UpdateQueue<mixed>, depth) {
|
||||
log(' '.repeat(depth + 1) + 'QUEUED UPDATES');
|
||||
const firstUpdate = updateQueue.first;
|
||||
if (!firstUpdate) {
|
||||
|
||||
@@ -109,7 +109,7 @@ export type Fiber = {|
|
||||
memoizedProps: any, // The props used to create the output.
|
||||
|
||||
// A queue of state updates and callbacks.
|
||||
updateQueue: UpdateQueue | null,
|
||||
updateQueue: UpdateQueue<any> | null,
|
||||
|
||||
// The state used to create the output
|
||||
memoizedState: any,
|
||||
|
||||
@@ -24,7 +24,7 @@ var {
|
||||
reconcileChildFibersInPlace,
|
||||
cloneChildFibers,
|
||||
} = require('ReactChildFiber');
|
||||
var {beginUpdateQueue} = require('ReactFiberUpdateQueue');
|
||||
var {processUpdateQueue} = require('ReactFiberUpdateQueue');
|
||||
var ReactTypeOfWork = require('ReactTypeOfWork');
|
||||
var {
|
||||
getMaskedContext,
|
||||
@@ -71,8 +71,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
|
||||
hostContext: HostContext<C, CX>,
|
||||
hydrationContext: HydrationContext<C, CX>,
|
||||
scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
|
||||
getExpirationTime: (fiber: Fiber, forceAsync: boolean) => ExpirationTime,
|
||||
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
|
||||
computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
|
||||
) {
|
||||
const {
|
||||
shouldSetTextContent,
|
||||
@@ -95,8 +95,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
// resumeMountClassInstance,
|
||||
updateClassInstance,
|
||||
} = ReactFiberClassComponent(
|
||||
scheduleUpdate,
|
||||
getExpirationTime,
|
||||
scheduleWork,
|
||||
computeExpirationForFiber,
|
||||
memoizeProps,
|
||||
memoizeState,
|
||||
);
|
||||
@@ -323,12 +323,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
const updateQueue = workInProgress.updateQueue;
|
||||
if (updateQueue !== null) {
|
||||
const prevState = workInProgress.memoizedState;
|
||||
const state = beginUpdateQueue(
|
||||
const state = processUpdateQueue(
|
||||
current,
|
||||
workInProgress,
|
||||
updateQueue,
|
||||
null,
|
||||
prevState,
|
||||
null,
|
||||
renderExpirationTime,
|
||||
);
|
||||
@@ -720,7 +719,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
function memoizeState(workInProgress: Fiber, nextState: any) {
|
||||
workInProgress.memoizedState = nextState;
|
||||
// Don't reset the updateQueue, in case there are pending updates. Resetting
|
||||
// is handled by beginUpdateQueue.
|
||||
// is handled by processUpdateQueue.
|
||||
}
|
||||
|
||||
function beginWork(
|
||||
|
||||
@@ -25,10 +25,8 @@ var {
|
||||
isContextConsumer,
|
||||
} = require('ReactFiberContext');
|
||||
var {
|
||||
addUpdate,
|
||||
addReplaceUpdate,
|
||||
addForceUpdate,
|
||||
beginUpdateQueue,
|
||||
insertUpdateIntoFiber,
|
||||
processUpdateQueue,
|
||||
} = require('ReactFiberUpdateQueue');
|
||||
var {hasContextChanged} = require('ReactFiberContext');
|
||||
var {isMounted} = require('ReactFiberTreeReflection');
|
||||
@@ -77,8 +75,8 @@ if (__DEV__) {
|
||||
}
|
||||
|
||||
module.exports = function(
|
||||
scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
|
||||
getExpirationTime: (fiber: Fiber, forceAsync: boolean) => ExpirationTime,
|
||||
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
|
||||
computeExpirationForFiber: (fiber: Fiber) => ExpirationTime,
|
||||
memoizeProps: (workInProgress: Fiber, props: any) => void,
|
||||
memoizeState: (workInProgress: Fiber, state: any) => void,
|
||||
) {
|
||||
@@ -87,33 +85,60 @@ module.exports = function(
|
||||
isMounted,
|
||||
enqueueSetState(instance, partialState, callback) {
|
||||
const fiber = ReactInstanceMap.get(instance);
|
||||
const expirationTime = getExpirationTime(fiber, false);
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (__DEV__) {
|
||||
warnOnInvalidCallback(callback, 'setState');
|
||||
}
|
||||
addUpdate(fiber, partialState, callback, expirationTime);
|
||||
scheduleUpdate(fiber, expirationTime);
|
||||
const expirationTime = computeExpirationForFiber(fiber);
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState,
|
||||
callback,
|
||||
isReplace: false,
|
||||
isForced: false,
|
||||
nextCallback: null,
|
||||
next: null,
|
||||
};
|
||||
insertUpdateIntoFiber(fiber, update);
|
||||
scheduleWork(fiber, expirationTime);
|
||||
},
|
||||
enqueueReplaceState(instance, state, callback) {
|
||||
const fiber = ReactInstanceMap.get(instance);
|
||||
const expirationTime = getExpirationTime(fiber, false);
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (__DEV__) {
|
||||
warnOnInvalidCallback(callback, 'replaceState');
|
||||
}
|
||||
addReplaceUpdate(fiber, state, callback, expirationTime);
|
||||
scheduleUpdate(fiber, expirationTime);
|
||||
const expirationTime = computeExpirationForFiber(fiber);
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState: state,
|
||||
callback,
|
||||
isReplace: true,
|
||||
isForced: false,
|
||||
nextCallback: null,
|
||||
next: null,
|
||||
};
|
||||
insertUpdateIntoFiber(fiber, update);
|
||||
scheduleWork(fiber, expirationTime);
|
||||
},
|
||||
enqueueForceUpdate(instance, callback) {
|
||||
const fiber = ReactInstanceMap.get(instance);
|
||||
const expirationTime = getExpirationTime(fiber, false);
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (__DEV__) {
|
||||
warnOnInvalidCallback(callback, 'forceUpdate');
|
||||
}
|
||||
addForceUpdate(fiber, callback, expirationTime);
|
||||
scheduleUpdate(fiber, expirationTime);
|
||||
const expirationTime = computeExpirationForFiber(fiber);
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState: null,
|
||||
callback,
|
||||
isReplace: false,
|
||||
isForced: true,
|
||||
nextCallback: null,
|
||||
next: null,
|
||||
};
|
||||
insertUpdateIntoFiber(fiber, update);
|
||||
scheduleWork(fiber, expirationTime);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -404,7 +429,7 @@ module.exports = function(
|
||||
const unmaskedContext = getUnmaskedContext(workInProgress);
|
||||
|
||||
instance.props = props;
|
||||
instance.state = state;
|
||||
instance.state = workInProgress.memoizedState = state;
|
||||
instance.refs = emptyObject;
|
||||
instance.context = getMaskedContext(workInProgress, unmaskedContext);
|
||||
|
||||
@@ -423,12 +448,11 @@ module.exports = function(
|
||||
// process them now.
|
||||
const updateQueue = workInProgress.updateQueue;
|
||||
if (updateQueue !== null) {
|
||||
instance.state = beginUpdateQueue(
|
||||
instance.state = processUpdateQueue(
|
||||
current,
|
||||
workInProgress,
|
||||
updateQueue,
|
||||
instance,
|
||||
state,
|
||||
props,
|
||||
renderExpirationTime,
|
||||
);
|
||||
@@ -481,7 +505,7 @@ module.exports = function(
|
||||
// // Process the update queue before calling shouldComponentUpdate
|
||||
// const updateQueue = workInProgress.updateQueue;
|
||||
// if (updateQueue !== null) {
|
||||
// newState = beginUpdateQueue(
|
||||
// newState = processUpdateQueue(
|
||||
// workInProgress,
|
||||
// updateQueue,
|
||||
// instance,
|
||||
@@ -524,7 +548,7 @@ module.exports = function(
|
||||
// // componentWillMount may have called setState. Process the update queue.
|
||||
// const newUpdateQueue = workInProgress.updateQueue;
|
||||
// if (newUpdateQueue !== null) {
|
||||
// newState = beginUpdateQueue(
|
||||
// newState = processUpdateQueue(
|
||||
// workInProgress,
|
||||
// newUpdateQueue,
|
||||
// instance,
|
||||
@@ -590,12 +614,11 @@ module.exports = function(
|
||||
// TODO: Previous state can be null.
|
||||
let newState;
|
||||
if (workInProgress.updateQueue !== null) {
|
||||
newState = beginUpdateQueue(
|
||||
newState = processUpdateQueue(
|
||||
current,
|
||||
workInProgress,
|
||||
workInProgress.updateQueue,
|
||||
instance,
|
||||
oldState,
|
||||
newProps,
|
||||
renderExpirationTime,
|
||||
);
|
||||
|
||||
@@ -30,12 +30,7 @@ var {
|
||||
clearCaughtError,
|
||||
} = require('ReactErrorUtils');
|
||||
|
||||
var {
|
||||
Placement,
|
||||
Update,
|
||||
Callback,
|
||||
ContentReset,
|
||||
} = require('ReactTypeOfSideEffect');
|
||||
var {Placement, Update, ContentReset} = require('ReactTypeOfSideEffect');
|
||||
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
|
||||
@@ -132,19 +127,19 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
finishedWork.effectTag & Callback &&
|
||||
finishedWork.updateQueue !== null
|
||||
) {
|
||||
commitCallbacks(finishedWork, finishedWork.updateQueue, instance);
|
||||
const updateQueue = finishedWork.updateQueue;
|
||||
if (updateQueue !== null) {
|
||||
commitCallbacks(updateQueue, instance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case HostRoot: {
|
||||
const updateQueue = finishedWork.updateQueue;
|
||||
if (updateQueue !== null) {
|
||||
const instance = finishedWork.child && finishedWork.child.stateNode;
|
||||
commitCallbacks(finishedWork, updateQueue, instance);
|
||||
const instance = finishedWork.child !== null
|
||||
? finishedWork.child.stateNode
|
||||
: null;
|
||||
commitCallbacks(updateQueue, instance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,27 +34,20 @@ function msToExpirationTime(ms: number): ExpirationTime {
|
||||
exports.msToExpirationTime = msToExpirationTime;
|
||||
|
||||
function ceiling(num: number, precision: number): number {
|
||||
return (((((num * precision) | 0) + 1) / precision) | 0) + 1;
|
||||
return (((num / precision) | 0) + 1) * precision;
|
||||
}
|
||||
|
||||
function bucket(
|
||||
function computeExpirationBucket(
|
||||
currentTime: ExpirationTime,
|
||||
expirationInMs: number,
|
||||
precisionInMs: number,
|
||||
bucketSizeMs: number,
|
||||
): ExpirationTime {
|
||||
return ceiling(
|
||||
currentTime + expirationInMs / UNIT_SIZE,
|
||||
precisionInMs / UNIT_SIZE,
|
||||
bucketSizeMs / UNIT_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
// Given the current clock time, returns an expiration time. We use rounding
|
||||
// to batch like updates together.
|
||||
function asyncExpirationTime(currentTime: ExpirationTime) {
|
||||
// Should complete within ~1000ms. 1200ms max.
|
||||
return bucket(currentTime, 1000, 200);
|
||||
}
|
||||
exports.asyncExpirationTime = asyncExpirationTime;
|
||||
exports.computeExpirationBucket = computeExpirationBucket;
|
||||
|
||||
// Given the current clock time and an expiration time, returns the
|
||||
// relative expiration time. Possible values include NoWork, Sync, Task, and
|
||||
|
||||
@@ -15,9 +15,6 @@ import type {FiberRoot} from 'ReactFiberRoot';
|
||||
import type {ReactNodeList} from 'ReactTypes';
|
||||
|
||||
var ReactFeatureFlags = require('ReactFeatureFlags');
|
||||
|
||||
var {addTopLevelUpdate} = require('ReactFiberUpdateQueue');
|
||||
|
||||
var {
|
||||
findCurrentUnmaskedContext,
|
||||
isContextProvider,
|
||||
@@ -27,6 +24,7 @@ var {createFiberRoot} = require('ReactFiberRoot');
|
||||
var ReactFiberScheduler = require('ReactFiberScheduler');
|
||||
var ReactInstanceMap = require('ReactInstanceMap');
|
||||
var {HostComponent} = require('ReactTypeOfWork');
|
||||
var {insertUpdateIntoFiber} = require('ReactFiberUpdateQueue');
|
||||
var emptyObject = require('fbjs/lib/emptyObject');
|
||||
|
||||
if (__DEV__) {
|
||||
@@ -265,8 +263,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
var {getPublicInstance} = config;
|
||||
|
||||
var {
|
||||
scheduleUpdate,
|
||||
getExpirationTime,
|
||||
computeAsyncExpiration,
|
||||
computeExpirationForFiber,
|
||||
scheduleWork,
|
||||
batchedUpdates,
|
||||
unbatchedUpdates,
|
||||
flushSync,
|
||||
@@ -296,17 +295,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the top-level element is an async wrapper component. If so, treat
|
||||
// updates to the root as async. This is a bit weird but lets us avoid a separate
|
||||
// `renderAsync` API.
|
||||
const forceAsync =
|
||||
ReactFeatureFlags.enableAsyncSubtreeAPI &&
|
||||
element != null &&
|
||||
element.type != null &&
|
||||
element.type.prototype != null &&
|
||||
(element.type.prototype: any).unstable_isAsyncReactComponent === true;
|
||||
const expirationTime = getExpirationTime(current, forceAsync);
|
||||
const nextState = {element};
|
||||
callback = callback === undefined ? null : callback;
|
||||
if (__DEV__) {
|
||||
warning(
|
||||
@@ -316,8 +304,34 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
callback,
|
||||
);
|
||||
}
|
||||
addTopLevelUpdate(current, nextState, callback, expirationTime);
|
||||
scheduleUpdate(current, expirationTime);
|
||||
|
||||
let expirationTime;
|
||||
// Check if the top-level element is an async wrapper component. If so,
|
||||
// treat updates to the root as async. This is a bit weird but lets us
|
||||
// avoid a separate `renderAsync` API.
|
||||
if (
|
||||
ReactFeatureFlags.enableAsyncSubtreeAPI &&
|
||||
element != null &&
|
||||
element.type != null &&
|
||||
element.type.prototype != null &&
|
||||
(element.type.prototype: any).unstable_isAsyncReactComponent === true
|
||||
) {
|
||||
expirationTime = computeAsyncExpiration();
|
||||
} else {
|
||||
expirationTime = computeExpirationForFiber(current);
|
||||
}
|
||||
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState: {element},
|
||||
callback,
|
||||
isReplace: false,
|
||||
isForced: false,
|
||||
nextCallback: null,
|
||||
next: null,
|
||||
};
|
||||
insertUpdateIntoFiber(current, update);
|
||||
scheduleWork(current, expirationTime);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -59,7 +59,7 @@ var {
|
||||
Sync,
|
||||
Never,
|
||||
msToExpirationTime,
|
||||
asyncExpirationTime,
|
||||
computeExpirationBucket,
|
||||
relativeExpirationTime,
|
||||
} = require('ReactFiberExpirationTime');
|
||||
|
||||
@@ -165,8 +165,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
config,
|
||||
hostContext,
|
||||
hydrationContext,
|
||||
scheduleUpdate,
|
||||
getExpirationTime,
|
||||
scheduleWork,
|
||||
computeExpirationForFiber,
|
||||
);
|
||||
const {completeWork} = ReactFiberCompleteWork(
|
||||
config,
|
||||
@@ -1373,11 +1373,58 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) {
|
||||
return scheduleUpdateImpl(fiber, expirationTime, false);
|
||||
function computeAsyncExpiration() {
|
||||
// Given the current clock time, returns an expiration time. We use rounding
|
||||
// to batch like updates together.
|
||||
// Should complete within ~1000ms. 1200ms max.
|
||||
const currentTime = recalculateCurrentTime();
|
||||
const expirationMs = 1000;
|
||||
const bucketSizeMs = 200;
|
||||
return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
|
||||
}
|
||||
|
||||
function scheduleUpdateImpl(
|
||||
function computeExpirationForFiber(fiber: Fiber) {
|
||||
let expirationTime;
|
||||
if (expirationContext !== NoWork) {
|
||||
// An explicit expiration context was set;
|
||||
expirationTime = expirationContext;
|
||||
} else if (isPerformingWork) {
|
||||
if (isCommitting) {
|
||||
// Updates that occur during the commit phase should have sync priority
|
||||
// by default.
|
||||
expirationTime = Sync;
|
||||
} else {
|
||||
// Updates during the render phase should expire at the same time as
|
||||
// the work that is being rendered.
|
||||
expirationTime = nextRenderExpirationTime;
|
||||
}
|
||||
} else {
|
||||
// No explicit expiration context was set, and we're not currently
|
||||
// performing work. Calculate a new expiration time.
|
||||
if (useSyncScheduling && !(fiber.internalContextTag & AsyncUpdates)) {
|
||||
// This is a sync update
|
||||
expirationTime = Sync;
|
||||
} else {
|
||||
// This is an async update
|
||||
expirationTime = computeAsyncExpiration();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
expirationTime === Sync &&
|
||||
(isBatchingUpdates || (isUnbatchingUpdates && isCommitting))
|
||||
) {
|
||||
// If we're in a batch, downgrade sync to task.
|
||||
expirationTime = Task;
|
||||
}
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
|
||||
return scheduleWorkImpl(fiber, expirationTime, false);
|
||||
}
|
||||
|
||||
function scheduleWorkImpl(
|
||||
fiber: Fiber,
|
||||
expirationTime: ExpirationTime,
|
||||
isErrorRecovery: boolean,
|
||||
@@ -1483,53 +1530,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
}
|
||||
}
|
||||
|
||||
function getExpirationTime(
|
||||
fiber: Fiber,
|
||||
forceAsync: boolean,
|
||||
): ExpirationTime {
|
||||
let expirationTime;
|
||||
if (expirationContext !== NoWork) {
|
||||
// An explicit expiration context was set;
|
||||
expirationTime = expirationContext;
|
||||
} else if (isPerformingWork) {
|
||||
if (isCommitting) {
|
||||
// Updates that occur during the commit phase should have task priority
|
||||
// by default.
|
||||
expirationTime = Sync;
|
||||
} else {
|
||||
// Updates during the render phase should expire at the same time as
|
||||
// the work that is being rendered.
|
||||
expirationTime = nextRenderExpirationTime;
|
||||
}
|
||||
} else {
|
||||
// No explicit expiration context was set, and we're not currently
|
||||
// performing work. Calculate a new expiration time.
|
||||
if (
|
||||
useSyncScheduling &&
|
||||
!(fiber.internalContextTag & AsyncUpdates) &&
|
||||
!forceAsync
|
||||
) {
|
||||
// This is a sync update
|
||||
expirationTime = Sync;
|
||||
} else {
|
||||
// This is an async update
|
||||
const currentTime = recalculateCurrentTime();
|
||||
expirationTime = asyncExpirationTime(currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
expirationTime === Sync &&
|
||||
(isBatchingUpdates || (isUnbatchingUpdates && isCommitting))
|
||||
) {
|
||||
// If we're in a batch, or in the commit phase, downgrade sync to task
|
||||
return Task;
|
||||
}
|
||||
return expirationTime;
|
||||
}
|
||||
|
||||
function scheduleErrorRecovery(fiber: Fiber) {
|
||||
scheduleUpdateImpl(fiber, Task, true);
|
||||
scheduleWorkImpl(fiber, Task, true);
|
||||
}
|
||||
|
||||
function recalculateCurrentTime(): ExpirationTime {
|
||||
@@ -1590,8 +1592,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
|
||||
function deferredUpdates<A>(fn: () => A): A {
|
||||
const previousExpirationContext = expirationContext;
|
||||
const currentTime = recalculateCurrentTime();
|
||||
expirationContext = asyncExpirationTime(currentTime);
|
||||
expirationContext = computeAsyncExpiration();
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
@@ -1600,8 +1601,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
}
|
||||
|
||||
return {
|
||||
scheduleUpdate: scheduleUpdate,
|
||||
getExpirationTime: getExpirationTime,
|
||||
computeAsyncExpiration: computeAsyncExpiration,
|
||||
computeExpirationForFiber: computeExpirationForFiber,
|
||||
scheduleWork: scheduleWork,
|
||||
batchedUpdates: batchedUpdates,
|
||||
unbatchedUpdates: unbatchedUpdates,
|
||||
flushSync: flushSync,
|
||||
|
||||
@@ -20,6 +20,7 @@ const {NoWork} = require('ReactFiberExpirationTime');
|
||||
const {ClassComponent, HostRoot} = require('ReactTypeOfWork');
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
if (__DEV__) {
|
||||
var warning = require('fbjs/lib/warning');
|
||||
}
|
||||
@@ -31,14 +32,13 @@ type PartialState<State, Props> =
|
||||
// Callbacks are not validated until invocation
|
||||
type Callback = mixed;
|
||||
|
||||
export type Update = {
|
||||
export type Update<State> = {
|
||||
expirationTime: ExpirationTime,
|
||||
partialState: PartialState<any, any>,
|
||||
callback: Callback | null,
|
||||
isReplace: boolean,
|
||||
isForced: boolean,
|
||||
isTopLevelUnmount: boolean,
|
||||
next: Update | null,
|
||||
next: Update<State> | null,
|
||||
};
|
||||
|
||||
// Singly linked-list of updates. When an update is scheduled, it is added to
|
||||
@@ -52,25 +52,31 @@ export type Update = {
|
||||
// The work-in-progress queue is always a subset of the current queue.
|
||||
//
|
||||
// When the tree is committed, the work-in-progress becomes the current.
|
||||
export type UpdateQueue = {
|
||||
first: Update | null,
|
||||
last: Update | null,
|
||||
export type UpdateQueue<State> = {
|
||||
// A processed update is not removed from the queue if there are any
|
||||
// unprocessed updates that came before it. In that case, we need to keep
|
||||
// track of the base state, which represents the base state of the first
|
||||
// unprocessed update, which is the same as the first update in the list.
|
||||
baseState: State,
|
||||
// For the same reason, we keep track of the remaining expiration time.
|
||||
expirationTime: ExpirationTime,
|
||||
first: Update<State> | null,
|
||||
last: Update<State> | null,
|
||||
callbackList: Array<Update<State>> | null,
|
||||
hasForceUpdate: boolean,
|
||||
callbackList: null | Array<Callback>,
|
||||
|
||||
// Dev only
|
||||
isProcessing?: boolean,
|
||||
};
|
||||
|
||||
let _queue1;
|
||||
let _queue2;
|
||||
|
||||
function createUpdateQueue(): UpdateQueue {
|
||||
const queue: UpdateQueue = {
|
||||
function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
|
||||
const queue: UpdateQueue<State> = {
|
||||
baseState,
|
||||
expirationTime: NoWork,
|
||||
first: null,
|
||||
last: null,
|
||||
hasForceUpdate: false,
|
||||
callbackList: null,
|
||||
hasForceUpdate: false,
|
||||
};
|
||||
if (__DEV__) {
|
||||
queue.isProcessing = false;
|
||||
@@ -78,120 +84,50 @@ function createUpdateQueue(): UpdateQueue {
|
||||
return queue;
|
||||
}
|
||||
|
||||
function cloneUpdate(update: Update): Update {
|
||||
return {
|
||||
expirationTime: update.expirationTime,
|
||||
partialState: update.partialState,
|
||||
callback: update.callback,
|
||||
isReplace: update.isReplace,
|
||||
isForced: update.isForced,
|
||||
isTopLevelUnmount: update.isTopLevelUnmount,
|
||||
next: null,
|
||||
};
|
||||
}
|
||||
|
||||
function insertUpdateIntoQueue(
|
||||
queue: UpdateQueue,
|
||||
update: Update,
|
||||
insertAfter: Update | null,
|
||||
insertBefore: Update | null,
|
||||
) {
|
||||
if (insertAfter !== null) {
|
||||
insertAfter.next = update;
|
||||
function insertUpdateIntoQueue<State>(
|
||||
queue: UpdateQueue<State>,
|
||||
update: Update<State>,
|
||||
): void {
|
||||
// Append the update to the end of the list.
|
||||
if (queue.last === null) {
|
||||
// Queue is empty
|
||||
queue.first = queue.last = update;
|
||||
} else {
|
||||
// This is the first item in the queue.
|
||||
update.next = queue.first;
|
||||
queue.first = update;
|
||||
}
|
||||
|
||||
if (insertBefore !== null) {
|
||||
update.next = insertBefore;
|
||||
} else {
|
||||
// This is the last item in the queue.
|
||||
queue.last.next = update;
|
||||
queue.last = update;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the update after which the incoming update should be inserted into
|
||||
// the queue, or null if it should be inserted at beginning.
|
||||
function findInsertionPosition(queue, update): Update | null {
|
||||
const expirationTime = update.expirationTime;
|
||||
let insertAfter = null;
|
||||
let insertBefore = null;
|
||||
if (queue.last !== null && queue.last.expirationTime <= expirationTime) {
|
||||
// Fast path for the common case where the update should be inserted at
|
||||
// the end of the queue.
|
||||
insertAfter = queue.last;
|
||||
} else {
|
||||
insertBefore = queue.first;
|
||||
while (
|
||||
insertBefore !== null &&
|
||||
insertBefore.expirationTime <= expirationTime
|
||||
) {
|
||||
insertAfter = insertBefore;
|
||||
insertBefore = insertBefore.next;
|
||||
}
|
||||
if (
|
||||
queue.expirationTime === NoWork ||
|
||||
queue.expirationTime > update.expirationTime
|
||||
) {
|
||||
queue.expirationTime = update.expirationTime;
|
||||
}
|
||||
return insertAfter;
|
||||
}
|
||||
exports.insertUpdateIntoQueue = insertUpdateIntoQueue;
|
||||
|
||||
function ensureUpdateQueues(fiber: Fiber) {
|
||||
function insertUpdateIntoFiber<State>(
|
||||
fiber: Fiber,
|
||||
update: Update<State>,
|
||||
): void {
|
||||
// We'll have at least one and at most two distinct update queues.
|
||||
const alternateFiber = fiber.alternate;
|
||||
|
||||
let queue1 = fiber.updateQueue;
|
||||
if (queue1 === null) {
|
||||
queue1 = fiber.updateQueue = createUpdateQueue();
|
||||
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
|
||||
}
|
||||
|
||||
let queue2;
|
||||
if (alternateFiber !== null) {
|
||||
queue2 = alternateFiber.updateQueue;
|
||||
if (queue2 === null) {
|
||||
queue2 = alternateFiber.updateQueue = createUpdateQueue();
|
||||
queue2 = alternateFiber.updateQueue = createUpdateQueue(
|
||||
alternateFiber.memoizedState,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
queue2 = null;
|
||||
}
|
||||
|
||||
_queue1 = queue1;
|
||||
// Return null if there is no alternate queue, or if its queue is the same.
|
||||
_queue2 = queue2 !== queue1 ? queue2 : null;
|
||||
}
|
||||
|
||||
// The work-in-progress queue is a subset of the current queue (if it exists).
|
||||
// We need to insert the incoming update into both lists. However, it's possible
|
||||
// that the correct position in one list will be different from the position in
|
||||
// the other. Consider the following case:
|
||||
//
|
||||
// Current: 3-5-6
|
||||
// Work-in-progress: 6
|
||||
//
|
||||
// Then we receive an update with priority 4 and insert it into each list:
|
||||
//
|
||||
// Current: 3-4-5-6
|
||||
// Work-in-progress: 4-6
|
||||
//
|
||||
// In the current queue, the new update's `next` pointer points to the update
|
||||
// with priority 5. But in the work-in-progress queue, the pointer points to the
|
||||
// update with priority 6. Because these two queues share the same persistent
|
||||
// data structure, this won't do. (This can only happen when the incoming update
|
||||
// has higher priority than all the updates in the work-in-progress queue.)
|
||||
//
|
||||
// To solve this, in the case where the incoming update needs to be inserted
|
||||
// into two different positions, we'll make a clone of the update and insert
|
||||
// each copy into a separate queue. This forks the list while maintaining a
|
||||
// persistent structure, because the update that is added to the work-in-progress
|
||||
// is always added to the front of the list.
|
||||
//
|
||||
// However, if incoming update is inserted into the same position of both lists,
|
||||
// we shouldn't make a copy.
|
||||
//
|
||||
// If the update is cloned, it returns the cloned update.
|
||||
function insertUpdate(fiber: Fiber, update: Update): Update | null {
|
||||
// We'll have at least one and at most two distinct update queues.
|
||||
ensureUpdateQueues(fiber);
|
||||
const queue1 = _queue1;
|
||||
const queue2 = _queue2;
|
||||
queue2 = queue2 !== queue1 ? queue2 : null;
|
||||
|
||||
// Warn if an update is scheduled from inside an updater function.
|
||||
if (__DEV__) {
|
||||
@@ -206,161 +142,40 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
|
||||
}
|
||||
}
|
||||
|
||||
// Find the insertion position in the first queue.
|
||||
const insertAfter1 = findInsertionPosition(queue1, update);
|
||||
const insertBefore1 = insertAfter1 !== null
|
||||
? insertAfter1.next
|
||||
: queue1.first;
|
||||
|
||||
// If there's only one queue, add the update to that queue and exit.
|
||||
if (queue2 === null) {
|
||||
// If there's no alternate queue, there's nothing else to do but insert.
|
||||
insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
|
||||
return null;
|
||||
insertUpdateIntoQueue(queue1, update);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is an alternate queue, find the insertion position.
|
||||
const insertAfter2 = findInsertionPosition(queue2, update);
|
||||
const insertBefore2 = insertAfter2 !== null
|
||||
? insertAfter2.next
|
||||
: queue2.first;
|
||||
|
||||
// Now we can insert into the first queue. This must come after finding both
|
||||
// insertion positions because it mutates the list.
|
||||
insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
|
||||
|
||||
// See if the insertion positions are equal. Be careful to only compare
|
||||
// non-null values.
|
||||
if (
|
||||
(insertBefore1 === insertBefore2 && insertBefore1 !== null) ||
|
||||
(insertAfter1 === insertAfter2 && insertAfter1 !== null)
|
||||
) {
|
||||
// The insertion positions are the same, so when we inserted into the first
|
||||
// queue, it also inserted into the alternate. All we need to do is update
|
||||
// the alternate queue's `first` and `last` pointers, in case they
|
||||
// have changed.
|
||||
if (insertAfter2 === null) {
|
||||
queue2.first = update;
|
||||
}
|
||||
if (insertBefore2 === null) {
|
||||
queue2.last = null;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// The insertion positions are different, so we need to clone the update and
|
||||
// insert the clone into the alternate queue.
|
||||
const update2 = cloneUpdate(update);
|
||||
insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
|
||||
return update2;
|
||||
// If either queue is empty, we need to add to both queues.
|
||||
if (queue1.last === null || queue2.last === null) {
|
||||
insertUpdateIntoQueue(queue1, update);
|
||||
insertUpdateIntoQueue(queue2, update);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function addUpdate(
|
||||
fiber: Fiber,
|
||||
partialState: PartialState<any, any> | null,
|
||||
callback: mixed,
|
||||
expirationTime: ExpirationTime,
|
||||
): void {
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState,
|
||||
callback,
|
||||
isReplace: false,
|
||||
isForced: false,
|
||||
isTopLevelUnmount: false,
|
||||
next: null,
|
||||
};
|
||||
insertUpdate(fiber, update);
|
||||
// If both lists are not empty, the last update is the same for both lists
|
||||
// because of structural sharing. So, we should only append to one of
|
||||
// the lists.
|
||||
insertUpdateIntoQueue(queue1, update);
|
||||
// But we still need to update the `last` pointer of queue2.
|
||||
queue2.last = update;
|
||||
}
|
||||
exports.addUpdate = addUpdate;
|
||||
|
||||
function addReplaceUpdate(
|
||||
fiber: Fiber,
|
||||
state: any | null,
|
||||
callback: Callback | null,
|
||||
expirationTime: ExpirationTime,
|
||||
): void {
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState: state,
|
||||
callback,
|
||||
isReplace: true,
|
||||
isForced: false,
|
||||
isTopLevelUnmount: false,
|
||||
next: null,
|
||||
};
|
||||
insertUpdate(fiber, update);
|
||||
}
|
||||
exports.addReplaceUpdate = addReplaceUpdate;
|
||||
|
||||
function addForceUpdate(
|
||||
fiber: Fiber,
|
||||
callback: Callback | null,
|
||||
expirationTime: ExpirationTime,
|
||||
): void {
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState: null,
|
||||
callback,
|
||||
isReplace: false,
|
||||
isForced: true,
|
||||
isTopLevelUnmount: false,
|
||||
next: null,
|
||||
};
|
||||
insertUpdate(fiber, update);
|
||||
}
|
||||
exports.addForceUpdate = addForceUpdate;
|
||||
exports.insertUpdateIntoFiber = insertUpdateIntoFiber;
|
||||
|
||||
function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
|
||||
if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
|
||||
return NoWork;
|
||||
}
|
||||
const updateQueue = fiber.updateQueue;
|
||||
if (updateQueue === null) {
|
||||
return NoWork;
|
||||
}
|
||||
if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
|
||||
return NoWork;
|
||||
}
|
||||
return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork;
|
||||
return updateQueue.expirationTime;
|
||||
}
|
||||
exports.getUpdateExpirationTime = getUpdateExpirationTime;
|
||||
|
||||
function addTopLevelUpdate(
|
||||
fiber: Fiber,
|
||||
partialState: PartialState<any, any>,
|
||||
callback: Callback | null,
|
||||
expirationTime: ExpirationTime,
|
||||
): void {
|
||||
const isTopLevelUnmount = partialState.element === null;
|
||||
|
||||
const update = {
|
||||
expirationTime,
|
||||
partialState,
|
||||
callback,
|
||||
isReplace: false,
|
||||
isForced: false,
|
||||
isTopLevelUnmount,
|
||||
next: null,
|
||||
};
|
||||
const update2 = insertUpdate(fiber, update);
|
||||
|
||||
if (isTopLevelUnmount) {
|
||||
// TODO: Redesign the top-level mount/update/unmount API to avoid this
|
||||
// special case.
|
||||
const queue1 = _queue1;
|
||||
const queue2 = _queue2;
|
||||
|
||||
// Drop all updates that are lower-priority, so that the tree is not
|
||||
// remounted. We need to do this for both queues.
|
||||
if (queue1 !== null && update.next !== null) {
|
||||
update.next = null;
|
||||
queue1.last = update;
|
||||
}
|
||||
if (queue2 !== null && update2 !== null && update2.next !== null) {
|
||||
update2.next = null;
|
||||
queue2.last = update;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.addTopLevelUpdate = addTopLevelUpdate;
|
||||
|
||||
function getStateFromUpdate(update, instance, prevState, props) {
|
||||
const partialState = update.partialState;
|
||||
if (typeof partialState === 'function') {
|
||||
@@ -371,19 +186,20 @@ function getStateFromUpdate(update, instance, prevState, props) {
|
||||
}
|
||||
}
|
||||
|
||||
function beginUpdateQueue(
|
||||
function processUpdateQueue<State>(
|
||||
current: Fiber | null,
|
||||
workInProgress: Fiber,
|
||||
queue: UpdateQueue,
|
||||
queue: UpdateQueue<State>,
|
||||
instance: any,
|
||||
prevState: any,
|
||||
props: any,
|
||||
renderExpirationTime: ExpirationTime,
|
||||
): any {
|
||||
): State {
|
||||
if (current !== null && current.updateQueue === queue) {
|
||||
// We need to create a work-in-progress queue, by cloning the current queue.
|
||||
const currentQueue = queue;
|
||||
queue = workInProgress.updateQueue = {
|
||||
baseState: currentQueue.baseState,
|
||||
expirationTime: currentQueue.expirationTime,
|
||||
first: currentQueue.first,
|
||||
last: currentQueue.last,
|
||||
// These fields are no longer valid because they were already committed.
|
||||
@@ -399,24 +215,47 @@ function beginUpdateQueue(
|
||||
queue.isProcessing = true;
|
||||
}
|
||||
|
||||
// Calculate these using the the existing values as a base.
|
||||
let callbackList = queue.callbackList;
|
||||
let hasForceUpdate = queue.hasForceUpdate;
|
||||
// Reset the remaining expiration time. If we skip over any updates, we'll
|
||||
// increase this accordingly.
|
||||
queue.expirationTime = NoWork;
|
||||
|
||||
// Applies updates with matching priority to the previous state to create
|
||||
// a new state object.
|
||||
let state = prevState;
|
||||
let state = queue.baseState;
|
||||
let dontMutatePrevState = true;
|
||||
let update = queue.first;
|
||||
while (update !== null && update.expirationTime <= renderExpirationTime) {
|
||||
// Remove each update from the queue right before it is processed. That way
|
||||
// if setState is called from inside an updater function, the new update
|
||||
// will be inserted in the correct position.
|
||||
queue.first = update.next;
|
||||
if (queue.first === null) {
|
||||
queue.last = null;
|
||||
let didSkip = false;
|
||||
while (update !== null) {
|
||||
const updateExpirationTime = update.expirationTime;
|
||||
if (updateExpirationTime > renderExpirationTime) {
|
||||
// This update does not have sufficient priority. Skip it.
|
||||
const remainingExpirationTime = queue.expirationTime;
|
||||
if (
|
||||
remainingExpirationTime === NoWork ||
|
||||
remainingExpirationTime > updateExpirationTime
|
||||
) {
|
||||
// Update the remaining expiration time.
|
||||
queue.expirationTime = updateExpirationTime;
|
||||
}
|
||||
if (!didSkip) {
|
||||
didSkip = true;
|
||||
queue.baseState = state;
|
||||
}
|
||||
// Continue to the next update.
|
||||
update = update.next;
|
||||
continue;
|
||||
}
|
||||
|
||||
// This update does have sufficient priority.
|
||||
|
||||
// If no previous updates were skipped, drop this update from the queue by
|
||||
// advancing the head of the list.
|
||||
if (!didSkip) {
|
||||
queue.first = update.next;
|
||||
if (queue.first === null) {
|
||||
queue.last = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the update
|
||||
let partialState;
|
||||
if (update.isReplace) {
|
||||
state = getStateFromUpdate(update, instance, state, props);
|
||||
@@ -425,6 +264,7 @@ function beginUpdateQueue(
|
||||
partialState = getStateFromUpdate(update, instance, state, props);
|
||||
if (partialState) {
|
||||
if (dontMutatePrevState) {
|
||||
// $FlowFixMe: Idk how to type this properly.
|
||||
state = Object.assign({}, state, partialState);
|
||||
} else {
|
||||
state = Object.assign(state, partialState);
|
||||
@@ -433,29 +273,31 @@ function beginUpdateQueue(
|
||||
}
|
||||
}
|
||||
if (update.isForced) {
|
||||
hasForceUpdate = true;
|
||||
queue.hasForceUpdate = true;
|
||||
}
|
||||
// Second condition ignores top-level unmount callbacks if they are not the
|
||||
// last update in the queue, since a subsequent update will cause a remount.
|
||||
if (
|
||||
update.callback !== null &&
|
||||
!(update.isTopLevelUnmount && update.next !== null)
|
||||
) {
|
||||
callbackList = callbackList !== null ? callbackList : [];
|
||||
callbackList.push(update.callback);
|
||||
workInProgress.effectTag |= CallbackEffect;
|
||||
if (update.callback !== null) {
|
||||
// Append to list of callbacks.
|
||||
let callbackList = queue.callbackList;
|
||||
if (callbackList === null) {
|
||||
callbackList = queue.callbackList = [];
|
||||
}
|
||||
callbackList.push(update);
|
||||
}
|
||||
update = update.next;
|
||||
}
|
||||
|
||||
queue.callbackList = callbackList;
|
||||
queue.hasForceUpdate = hasForceUpdate;
|
||||
|
||||
if (queue.first === null && callbackList === null && !hasForceUpdate) {
|
||||
// The queue is empty and there are no callbacks. We can reset it.
|
||||
if (queue.callbackList !== null) {
|
||||
workInProgress.effectTag |= CallbackEffect;
|
||||
} else if (queue.first === null && !queue.hasForceUpdate) {
|
||||
// The queue is empty. We can reset it.
|
||||
workInProgress.updateQueue = null;
|
||||
}
|
||||
|
||||
if (!didSkip) {
|
||||
didSkip = true;
|
||||
queue.baseState = state;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
// No longer processing.
|
||||
queue.isProcessing = false;
|
||||
@@ -463,23 +305,21 @@ function beginUpdateQueue(
|
||||
|
||||
return state;
|
||||
}
|
||||
exports.beginUpdateQueue = beginUpdateQueue;
|
||||
exports.processUpdateQueue = processUpdateQueue;
|
||||
|
||||
function commitCallbacks(
|
||||
finishedWork: Fiber,
|
||||
queue: UpdateQueue,
|
||||
context: mixed,
|
||||
) {
|
||||
function commitCallbacks<State>(queue: UpdateQueue<State>, context: any) {
|
||||
const callbackList = queue.callbackList;
|
||||
if (callbackList === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the list to null to make sure they don't get called more than once.
|
||||
queue.callbackList = null;
|
||||
|
||||
for (let i = 0; i < callbackList.length; i++) {
|
||||
const callback = callbackList[i];
|
||||
const update = callbackList[i];
|
||||
const callback = update.callback;
|
||||
// This update might be processed again. Clear the callback so it's only
|
||||
// called once.
|
||||
update.callback = null;
|
||||
invariant(
|
||||
typeof callback === 'function',
|
||||
'Invalid argument passed as callback. Expected a function. Instead ' +
|
||||
|
||||
@@ -57,11 +57,13 @@ describe('ReactIncrementalScheduling', () => {
|
||||
ReactNoop.render(<span prop={4} />);
|
||||
});
|
||||
});
|
||||
// The sync updates flush first.
|
||||
expect(ReactNoop.getChildren()).toEqual([span(4)]);
|
||||
|
||||
// The low pri update should be flushed last, even though it was scheduled
|
||||
// before the sync updates.
|
||||
// The terminal value should be the last update that was scheduled,
|
||||
// regardless of priority. In this case, that's the last sync update.
|
||||
ReactNoop.flush();
|
||||
expect(ReactNoop.getChildren()).toEqual([span(5)]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span(4)]);
|
||||
});
|
||||
|
||||
it('schedules top-level updates with same priority in order of insertion', () => {
|
||||
|
||||
@@ -246,6 +246,7 @@ describe('ReactIncrementalTriangle', () => {
|
||||
simulate(step(1), flush(3), toggle(18), step(0));
|
||||
simulate(step(4), flush(52), expire(1476), flush(17), step(0));
|
||||
simulate(interrupt(), toggle(10), step(2), expire(990), flush(46));
|
||||
simulate(interrupt(), step(6), step(7), toggle(6), interrupt());
|
||||
});
|
||||
|
||||
it('fuzz tester', () => {
|
||||
|
||||
@@ -154,19 +154,20 @@ describe('ReactIncrementalUpdates', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('')]);
|
||||
|
||||
// Schedule some more updates at different priorities{
|
||||
instance.setState(createUpdate('f'));
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('d'));
|
||||
instance.setState(createUpdate('e'));
|
||||
instance.setState(createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
|
||||
// The sync updates should have flushed, but not the async ones
|
||||
expect(ReactNoop.getChildren()).toEqual([span('de')]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('ef')]);
|
||||
|
||||
// Now flush the remaining work. Updates a, b, and c should be processed
|
||||
// again, since they were interrupted last time.
|
||||
expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'f', 'g']);
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
|
||||
});
|
||||
|
||||
@@ -202,23 +203,24 @@ describe('ReactIncrementalUpdates', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([span('')]);
|
||||
|
||||
// Schedule some more updates at different priorities{
|
||||
instance.setState(createUpdate('f'));
|
||||
instance.setState(createUpdate('d'));
|
||||
ReactNoop.flushSync(() => {
|
||||
instance.setState(createUpdate('d'));
|
||||
instance.setState(createUpdate('e'));
|
||||
// No longer a public API, but we can test that it works internally by
|
||||
// reaching into the updater.
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('e'));
|
||||
instance.updater.enqueueReplaceState(instance, createUpdate('f'));
|
||||
});
|
||||
instance.setState(createUpdate('g'));
|
||||
|
||||
// The sync updates should have flushed, but not the async ones. Update d
|
||||
// was dropped and replaced by e.
|
||||
expect(ReactNoop.getChildren()).toEqual([span('e')]);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('f')]);
|
||||
|
||||
// Now flush the remaining work. Updates a, b, and c should be processed
|
||||
// again, since they were interrupted last time.
|
||||
expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('abcefg')]);
|
||||
// Now flush the remaining work. Even though e and f were already processed,
|
||||
// they should be processed again, to ensure that the terminal state
|
||||
// is deterministic.
|
||||
expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
|
||||
});
|
||||
|
||||
it('passes accumulation of previous updates to replaceState updater function', () => {
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -2614,6 +2614,12 @@ istanbul-reports@^1.1.1:
|
||||
dependencies:
|
||||
handlebars "^4.0.3"
|
||||
|
||||
jasmine-check@^1.0.0-rc.0:
|
||||
version "1.0.0-rc.0"
|
||||
resolved "https://registry.yarnpkg.com/jasmine-check/-/jasmine-check-1.0.0-rc.0.tgz#117728c150078ecf211986c5f164275b71e937a4"
|
||||
dependencies:
|
||||
testcheck "^1.0.0-rc"
|
||||
|
||||
jest-changed-files@20.1.0-delta.1:
|
||||
version "20.1.0-delta.1"
|
||||
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:
|
||||
read-pkg-up "^1.0.1"
|
||||
require-main-filename "^1.0.1"
|
||||
|
||||
testcheck@^1.0.0-rc:
|
||||
version "1.0.0-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/testcheck/-/testcheck-1.0.0-rc.2.tgz#11356a25b84575efe0b0857451e85b5fa74ee4e4"
|
||||
|
||||
text-table@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
|
||||
Reference in New Issue
Block a user