Merge pull request #8538 from acdlite/fiberupdatequeue

[Fiber] Separate priority for updates
This commit is contained in:
Andrew Clark
2016-12-15 16:41:53 -08:00
committed by GitHub
16 changed files with 1223 additions and 267 deletions
+141 -13
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Fiber Example</title>
@@ -19,26 +19,154 @@
</div>
<script src="../../build/react.js"></script>
<script src="../../build/react-dom-fiber.js"></script>
<script>
function ExampleApplication(props) {
var elapsed = Math.round(props.elapsed / 100);
var seconds = elapsed / 10 + (elapsed % 10 ? '' : '.0' );
var message =
'React has been successfully running for ' + seconds + ' seconds.';
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.min.js"></script>
<script type="text/babel">
var dotStyle = {
position: 'absolute',
background: '#61dafb',
font: 'normal 15px sans-serif',
textAlign: 'center',
cursor: 'pointer',
};
return React.DOM.p(null, message);
var containerStyle = {
position: 'absolute',
transformOrigin: '0 0',
left: '50%',
top: '50%',
width: '10px',
height: '10px',
background: '#eee',
};
var targetSize = 25;
class Dot extends React.Component {
constructor() {
super();
this.state = { hover: false };
}
enter() {
this.setState({
hover: true
});
}
leave() {
this.setState({
hover: false
});
}
render() {
var props = this.props;
var s = props.size * 1.3;
var style = {
...dotStyle,
width: s + 'px',
height: s + 'px',
left: (props.x) + 'px',
top: (props.y) + 'px',
borderRadius: (s / 2) + 'px',
lineHeight: (s) + 'px',
background: this.state.hover ? '#ff0' : dotStyle.background
};
return (
<div style={style} onMouseEnter={() => this.enter()} onMouseLeave={() => this.leave()}>
{this.state.hover ? '*' + props.text + '*' : props.text}
</div>
);
}
}
// Call React.createFactory instead of directly call ExampleApplication({...}) in React.render
var ExampleApplicationFactory = React.createFactory(ExampleApplication);
function SierpinskiTriangle({ x, y, s, children }) {
if (s <= targetSize) {
return (
<Dot
x={x - (targetSize / 2)}
y={y - (targetSize / 2)}
size={targetSize}
text={children}
/>
);
return r;
}
var newSize = s / 2;
var slowDown = false;
if (slowDown) {
var e = performance.now() + 0.8;
while (performance.now() < e) {
// Artificially long execution time.
}
}
s /= 2;
return [
<SierpinskiTriangle x={x} y={y - (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x - s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x + s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
];
}
SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) {
var o = oldProps;
var n = newProps;
return !(
o.x === n.x &&
o.y === n.y &&
o.s === n.s &&
o.children === n.children
);
};
class ExampleApplication extends React.Component {
constructor() {
super();
this.state = { seconds: 0 };
this.tick = this.tick.bind(this);
}
componentDidMount() {
this.invervalID = setInterval(this.tick, 1000);
}
tick() {
ReactDOMFiber.unstable_deferredUpdates(() =>
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }))
);
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
render() {
const seconds = this.state.seconds;
const elapsed = this.props.elapsed;
const t = (elapsed / 1000) % 10;
const scale = 1 + (t > 5 ? 10 - t : t) / 10;
const transform = 'scaleX(' + (scale / 2.1) + ') scaleY(0.7) translateZ(0.1px)';
return (
<div style={{ ...containerStyle, transform }}>
<div>
<SierpinskiTriangle x={0} y={0} s={1000}>
{this.state.seconds}
</SierpinskiTriangle>
</div>
</div>
);
}
}
var start = new Date().getTime();
setInterval(function() {
function update() {
ReactDOMFiber.render(
ExampleApplicationFactory({elapsed: new Date().getTime() - start}),
<ExampleApplication elapsed={new Date().getTime() - start} />,
document.getElementById('container')
);
}, 50);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
</script>
</body>
</html>
-5
View File
@@ -71,13 +71,8 @@ src/renderers/shared/shared/__tests__/ReactComponentLifeCycle-test.js
* should carry through each of the phases of setup
src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js
* should warn about `setState` in render
* should warn about `setState` in getChildContext
* should update refs if shouldComponentUpdate gives false
src/renderers/shared/shared/__tests__/ReactCompositeComponentState-test.js
* should update state when called from child cWRP
src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js
* should still throw when rendering to undefined
* throws when rendering null at the top level
@@ -114,6 +114,8 @@ src/renderers/shared/shared/__tests__/ReactComponentLifeCycle-test.js
src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js
* should warn about `forceUpdate` on unmounted components
* should warn about `setState` on unmounted components
* should warn about `setState` in render
* should warn about `setState` in getChildContext
* should disallow nested render calls
src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
+11
View File
@@ -1230,6 +1230,16 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
* invokes ref callbacks after insertion/update/unmount
* supports string refs
src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
* applies updates in order of priority
* applies updates with equal priority in insertion order
* only drops updates with equal or lesser priority when replaceState is called
* can abort an update, schedule additional updates, and resume
* can abort an update, schedule a replaceState, and resume
* does not call callbacks that are scheduled by another callback until a later commit
* gives setState during reconciliation the same priority as whatever level is currently reconciling
* enqueues setState inside an updater function as if the in-progress update is progressed (and warns)
src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-test.js
* should render a simple fragment at the top of a component
* should preserve state when switching from a single child
@@ -1382,6 +1392,7 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponentState-test.js
* should support setting state
* should call componentDidUpdate of children first
* should batch unmounts
* should update state when called from child cWRP
src/renderers/shared/shared/__tests__/ReactEmptyComponent-test.js
* should not produce child DOM nodes for null and false
+2
View File
@@ -242,6 +242,8 @@ var ReactDOM = {
unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
unstable_deferredUpdates: DOMRenderer.deferredUpdates,
};
module.exports = ReactDOM;
+9 -6
View File
@@ -284,17 +284,20 @@ var ReactNoop = {
function logUpdateQueue(updateQueue : UpdateQueue, depth) {
log(
' '.repeat(depth + 1) + 'QUEUED UPDATES',
updateQueue.isReplace ? 'is replace' : '',
updateQueue.isForced ? 'is forced' : ''
' '.repeat(depth + 1) + 'QUEUED UPDATES'
);
const firstUpdate = updateQueue.first;
if (!firstUpdate) {
return;
}
log(
' '.repeat(depth + 1) + '~',
updateQueue.partialState,
updateQueue.callback ? 'with callback' : ''
firstUpdate && firstUpdate.partialState,
firstUpdate.callback ? 'with callback' : ''
);
var next;
while (next = updateQueue.next) {
while (next = firstUpdate.next) {
log(
' '.repeat(depth + 1) + '~',
next.partialState,
+12 -8
View File
@@ -43,6 +43,10 @@ var {
NoEffect,
} = require('ReactTypeOfSideEffect');
var {
cloneUpdateQueue,
} = require('ReactFiberUpdateQueue');
var invariant = require('invariant');
// A Fiber is work on a Component that needs to be done or was done. There can
@@ -100,12 +104,13 @@ export type Fiber = {
pendingProps: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge pendingProps and memoizedProps.
memoizedProps: any, // The props used to create the output.
// A queue of local state updates.
updateQueue: ?UpdateQueue,
// The state used to create the output. This is a full state object.
// A queue of state updates and callbacks.
updateQueue: UpdateQueue | null,
// A list of callbacks that should be called during the next commit.
callbackList: UpdateQueue | null,
// The state used to create the output
memoizedState: any,
// Linked list of callbacks to call after updates are committed.
callbackList: ?UpdateQueue,
// Effect
effectTag: TypeOfSideEffect,
@@ -194,8 +199,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
pendingProps: null,
memoizedProps: null,
updateQueue: null,
memoizedState: null,
callbackList: null,
memoizedState: null,
effectTag: NoEffect,
nextEffect: null,
@@ -270,8 +275,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
// pendingProps is here for symmetry but is unnecessary in practice for now.
// TODO: Pass in the new pendingProps as an argument maybe?
alt.pendingProps = fiber.pendingProps;
alt.updateQueue = fiber.updateQueue;
alt.callbackList = fiber.callbackList;
cloneUpdateQueue(alt, fiber);
alt.pendingWorkPriority = priorityLevel;
alt.memoizedProps = fiber.memoizedProps;
@@ -25,7 +25,10 @@ var {
reconcileChildFibersInPlace,
cloneChildFibers,
} = require('ReactChildFiber');
var {
hasPendingUpdate,
beginUpdateQueue,
} = require('ReactFiberUpdateQueue');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
getMaskedContext,
@@ -53,6 +56,7 @@ var {
OffscreenPriority,
} = require('ReactPriorityLevel');
var {
Update,
Placement,
ContentReset,
Err,
@@ -67,7 +71,10 @@ if (__DEV__) {
module.exports = function<T, P, I, TI, C, CX>(
config : HostConfig<T, P, I, TI, C, CX>,
hostContext : HostContext<C, CX>,
scheduleUpdate : (fiber: Fiber) => void
scheduleSetState: (fiber : Fiber, partialState : any) => void,
scheduleReplaceState: (fiber : Fiber, state : any) => void,
scheduleForceUpdate: (fiber : Fiber) => void,
scheduleUpdateCallback: (fiber : Fiber, callback : Function) => void,
) {
const { shouldSetTextContent } = config;
@@ -84,7 +91,12 @@ module.exports = function<T, P, I, TI, C, CX>(
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
} = ReactFiberClassComponent(scheduleUpdate);
} = ReactFiberClassComponent(
scheduleSetState,
scheduleReplaceState,
scheduleForceUpdate,
scheduleUpdateCallback
);
function markChildAsProgressed(current, workInProgress, priorityLevel) {
// We now have clones. Let's store them as the currently progressed work.
@@ -195,24 +207,38 @@ module.exports = function<T, P, I, TI, C, CX>(
return workInProgress.child;
}
function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
function updateClassComponent(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) {
let shouldUpdate;
if (!current) {
if (!workInProgress.stateNode) {
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress);
mountClassInstance(workInProgress);
mountClassInstance(workInProgress, priorityLevel);
shouldUpdate = true;
} else {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(workInProgress);
shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel);
}
} else {
shouldUpdate = updateClassInstance(current, workInProgress);
shouldUpdate = updateClassInstance(current, workInProgress, priorityLevel);
}
if (!shouldUpdate) {
// Schedule side-effects
if (shouldUpdate) {
workInProgress.effectTag |= Update;
} else {
// If an update was already in progress, we should schedule an Update
// effect even though we're bailing out, so that cWU/cDU are called.
if (current) {
const instance = current.stateNode;
if (instance.props !== current.memoizedProps ||
instance.state !== current.memoizedState) {
workInProgress.effectTag |= Update;
}
}
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
// Rerender
const instance = workInProgress.stateNode;
ReactCurrentOwner.current = workInProgress;
@@ -287,7 +313,7 @@ module.exports = function<T, P, I, TI, C, CX>(
}
}
function mountIndeterminateComponent(current, workInProgress) {
function mountIndeterminateComponent(current, workInProgress, priorityLevel) {
if (current) {
throw new Error('An indeterminate component should never have mounted.');
}
@@ -308,7 +334,7 @@ module.exports = function<T, P, I, TI, C, CX>(
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress);
mountClassInstance(workInProgress, priorityLevel);
ReactCurrentOwner.current = workInProgress;
value = value.render();
} else {
@@ -471,22 +497,31 @@ module.exports = function<T, P, I, TI, C, CX>(
workInProgress.child = workInProgress.progressedChild;
}
if ((workInProgress.pendingProps === null || (
workInProgress.memoizedProps !== null &&
workInProgress.pendingProps === workInProgress.memoizedProps
)) &&
workInProgress.updateQueue === null &&
!hasContextChanged()) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
const pendingProps = workInProgress.pendingProps;
const memoizedProps = workInProgress.memoizedProps;
const updateQueue = workInProgress.updateQueue;
// This is kept as a single expression to take advantage of short-circuiting.
const hasNewProps = (
pendingProps !== null && ( // hasPendingProps && (
memoizedProps === null || // hasNoMemoizedProps ||
pendingProps !== memoizedProps // memoizedPropsDontMatch
) // )
);
if (!hasNewProps) {
const hasUpdate = updateQueue && hasPendingUpdate(updateQueue, priorityLevel);
if (!hasUpdate && !hasContextChanged()) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
}
switch (workInProgress.tag) {
case IndeterminateComponent:
return mountIndeterminateComponent(current, workInProgress);
return mountIndeterminateComponent(current, workInProgress, priorityLevel);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress);
case ClassComponent:
return updateClassComponent(current, workInProgress);
return updateClassComponent(current, workInProgress, priorityLevel);
case HostRoot: {
const root = (workInProgress.stateNode : FiberRoot);
if (root.pendingContext) {
@@ -497,8 +532,14 @@ module.exports = function<T, P, I, TI, C, CX>(
} else {
pushTopLevelContextObject(root.context, false);
}
if (updateQueue) {
beginUpdateQueue(workInProgress, updateQueue, null, null, null, priorityLevel);
}
pushHostContainer(workInProgress.stateNode.containerInfo);
reconcileChildren(current, workInProgress, workInProgress.pendingProps);
reconcileChildren(current, workInProgress, pendingProps);
// A yield component is just a placeholder, we can just run through the
// next one immediately.
return workInProgress.child;
@@ -13,17 +13,15 @@
'use strict';
import type { Fiber } from 'ReactFiber';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
import type { PriorityLevel } from 'ReactPriorityLevel';
var {
getMaskedContext,
} = require('ReactFiberContext');
var {
createUpdateQueue,
addToQueue,
addCallbackToQueue,
mergeUpdateQueue,
beginUpdateQueue,
} = require('ReactFiberUpdateQueue');
var { hasContextChanged } = require('ReactFiberContext');
var { getComponentName, isMounted } = require('ReactFiberTreeReflection');
var ReactInstanceMap = require('ReactInstanceMap');
var shallowEqual = require('shallowEqual');
@@ -32,53 +30,37 @@ var invariant = require('invariant');
const isArray = Array.isArray;
module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
function scheduleUpdateQueue(fiber: Fiber, updateQueue: UpdateQueue) {
fiber.updateQueue = updateQueue;
// Schedule update on the alternate as well, since we don't know which tree
// is current.
if (fiber.alternate) {
fiber.alternate.updateQueue = updateQueue;
}
scheduleUpdate(fiber);
}
module.exports = function(
scheduleSetState: (fiber : Fiber, partialState : any) => void,
scheduleReplaceState: (fiber : Fiber, state : any) => void,
scheduleForceUpdate: (fiber : Fiber) => void,
scheduleUpdateCallback: (fiber : Fiber, callback : Function) => void,
) {
// Class component state updater
const updater = {
isMounted,
enqueueSetState(instance, partialState) {
const fiber = ReactInstanceMap.get(instance);
const updateQueue = fiber.updateQueue ?
addToQueue(fiber.updateQueue, partialState) :
createUpdateQueue(partialState);
scheduleUpdateQueue(fiber, updateQueue);
scheduleSetState(fiber, partialState);
},
enqueueReplaceState(instance, state) {
const fiber = ReactInstanceMap.get(instance);
const updateQueue = createUpdateQueue(state);
updateQueue.isReplace = true;
scheduleUpdateQueue(fiber, updateQueue);
scheduleReplaceState(fiber, state);
},
enqueueForceUpdate(instance) {
const fiber = ReactInstanceMap.get(instance);
const updateQueue = fiber.updateQueue || createUpdateQueue(null);
updateQueue.isForced = true;
scheduleUpdateQueue(fiber, updateQueue);
scheduleForceUpdate(fiber);
},
enqueueCallback(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
let updateQueue = fiber.updateQueue ?
fiber.updateQueue :
createUpdateQueue(null);
addCallbackToQueue(updateQueue, callback);
scheduleUpdateQueue(fiber, updateQueue);
scheduleUpdateCallback(fiber, callback);
},
};
function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) {
const updateQueue = workInProgress.updateQueue;
if (oldProps === null || (updateQueue && updateQueue.isForced)) {
if (oldProps === null || (workInProgress.updateQueue && workInProgress.updateQueue.hasForceUpdate)) {
// If the workInProgress already has an Update effect, return true
return true;
}
@@ -226,7 +208,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
}
// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(workInProgress : Fiber) : void {
function mountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : void {
const instance = workInProgress.stateNode;
const state = instance.state || null;
@@ -245,14 +227,21 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
// process them now.
const updateQueue = workInProgress.updateQueue;
if (updateQueue) {
instance.state = mergeUpdateQueue(updateQueue, instance, state, props);
instance.state = beginUpdateQueue(
workInProgress,
updateQueue,
instance,
state,
props,
priorityLevel
);
}
}
}
// Called on a preexisting class instance. Returns false if a resumed render
// could be reused.
function resumeMountClassInstance(workInProgress : Fiber) : boolean {
function resumeMountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean {
let newState = workInProgress.memoizedState;
let newProps = workInProgress.pendingProps;
if (!newProps) {
@@ -294,13 +283,20 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
// during initial mounting.
const newUpdateQueue = workInProgress.updateQueue;
if (newUpdateQueue) {
newInstance.state = mergeUpdateQueue(newUpdateQueue, newInstance, newState, newProps);
newInstance.state = beginUpdateQueue(
workInProgress,
newUpdateQueue,
newInstance,
newState,
newProps,
priorityLevel
);
}
return true;
}
// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(current : Fiber, workInProgress : Fiber) : boolean {
function updateClassInstance(current : Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps || current.memoizedProps;
@@ -332,19 +328,22 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
// TODO: Previous state can be null.
let newState;
if (updateQueue) {
if (!updateQueue.hasUpdate) {
newState = oldState;
} else {
newState = mergeUpdateQueue(updateQueue, instance, oldState, newProps);
}
newState = beginUpdateQueue(
workInProgress,
updateQueue,
instance,
oldState,
newProps,
priorityLevel
);
} else {
newState = oldState;
}
if (oldProps === newProps &&
oldState === newState &&
oldContext === newContext &&
updateQueue && !updateQueue.isForced) {
!hasContextChanged() &&
!(updateQueue && updateQueue.hasForceUpdate)) {
return false;
}
@@ -25,12 +25,11 @@ var {
HostPortal,
CoroutineComponent,
} = ReactTypeOfWork;
var { callCallbacks } = require('ReactFiberUpdateQueue');
var { commitCallbacks } = require('ReactFiberUpdateQueue');
var {
Placement,
Update,
Callback,
ContentReset,
} = require('ReactTypeOfSideEffect');
@@ -418,25 +417,17 @@ module.exports = function<T, P, I, TI, C, CX>(
}
attachRef(current, finishedWork, instance);
}
// Clear updates from current fiber.
if (finishedWork.alternate) {
finishedWork.alternate.updateQueue = null;
}
if (finishedWork.effectTag & Callback) {
if (finishedWork.callbackList) {
const callbackList = finishedWork.callbackList;
finishedWork.callbackList = null;
callCallbacks(callbackList, instance);
}
const callbackList = finishedWork.callbackList;
if (callbackList) {
commitCallbacks(finishedWork, callbackList, instance);
}
return;
}
case HostRoot: {
const rootFiber = finishedWork.stateNode;
if (rootFiber.callbackList) {
const callbackList = rootFiber.callbackList;
rootFiber.callbackList = null;
callCallbacks(callbackList, rootFiber.current.child.stateNode);
const callbackList = finishedWork.callbackList;
if (callbackList) {
const instance = finishedWork.child && finishedWork.child.stateNode;
commitCallbacks(finishedWork, callbackList, instance);
}
return;
}
@@ -41,7 +41,6 @@ var {
} = ReactTypeOfWork;
var {
Update,
Callback,
} = ReactTypeOfSideEffect;
if (__DEV__) {
@@ -73,11 +72,6 @@ module.exports = function<T, P, I, TI, C, CX>(
workInProgress.effectTag |= Update;
}
function markCallback(workInProgress : Fiber) {
// Tag the fiber with a callback effect.
workInProgress.effectTag |= Callback;
}
function appendAllYields(yields : Array<ReifiedYield>, workInProgress : Fiber) {
let node = workInProgress.child;
while (node) {
@@ -179,7 +173,7 @@ module.exports = function<T, P, I, TI, C, CX>(
case FunctionalComponent:
workInProgress.memoizedProps = workInProgress.pendingProps;
return null;
case ClassComponent:
case ClassComponent: {
// We are leaving this subtree, so pop context if any.
if (isContextProvider(workInProgress)) {
popContextProvider();
@@ -187,27 +181,13 @@ module.exports = function<T, P, I, TI, C, CX>(
// Don't use the state queue to compute the memoized state. We already
// merged it and assigned it to the instance. Transfer it from there.
// Also need to transfer the props, because pendingProps will be null
// in the case of an update
const { state, props } = workInProgress.stateNode;
const updateQueue = workInProgress.updateQueue;
workInProgress.memoizedState = state;
workInProgress.memoizedProps = props;
if (current) {
if (current.memoizedProps !== workInProgress.memoizedProps ||
current.memoizedState !== workInProgress.memoizedState ||
updateQueue && updateQueue.isForced) {
markUpdate(workInProgress);
}
} else {
markUpdate(workInProgress);
}
if (updateQueue && updateQueue.hasCallback) {
// Transfer update queue to callbackList field so callbacks can be
// called during commit phase.
workInProgress.callbackList = updateQueue;
markCallback(workInProgress);
}
// in the case of an update.
const instance = workInProgress.stateNode;
workInProgress.memoizedState = instance.state;
workInProgress.memoizedProps = instance.props;
return null;
}
case HostRoot: {
workInProgress.memoizedProps = workInProgress.pendingProps;
const fiberRoot = (workInProgress.stateNode : FiberRoot);
@@ -215,9 +195,6 @@ module.exports = function<T, P, I, TI, C, CX>(
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
// TODO: Only mark this as an update if we have any pending callbacks
// on it.
markUpdate(workInProgress);
return null;
}
case HostComponent:
@@ -24,8 +24,6 @@ var {
var { createFiberRoot } = require('ReactFiberRoot');
var ReactFiberScheduler = require('ReactFiberScheduler');
var { createUpdateQueue, addCallbackToQueue } = require('ReactFiberUpdateQueue');
if (__DEV__) {
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
}
@@ -79,6 +77,7 @@ export type Reconciler<C, I, TI> = {
// FIXME: ESLint complains about type parameter
batchedUpdates<A>(fn : () => A) : A,
syncUpdates<A>(fn : () => A) : A,
deferredUpdates<A>(fn : () => A) : A,
/* eslint-enable no-undef */
// Used to extract the return value from the initial render. Legacy API.
@@ -99,9 +98,11 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
var {
scheduleWork,
scheduleUpdateCallback,
performWithPriority,
batchedUpdates,
syncUpdates,
deferredUpdates,
} = ReactFiberScheduler(config);
return {
@@ -109,16 +110,19 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
mountContainer(element : ReactElement<any>, containerInfo : C, parentComponent : ?ReactComponent<any, any, any>, callback: ?Function) : OpaqueNode {
const context = getContextForSubtree(parentComponent);
const root = createFiberRoot(containerInfo, context);
const container = root.current;
if (callback) {
const queue = createUpdateQueue(null);
addCallbackToQueue(queue, callback);
root.callbackList = queue;
}
// TODO: Use pending work/state instead of props.
const current = root.current;
// TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps.
// TODO: This should not override the pendingWorkPriority if there is
// higher priority work in the subtree.
container.pendingProps = element;
current.pendingProps = element;
if (current.alternate) {
current.alternate.pendingProps = element;
}
if (callback) {
scheduleUpdateCallback(current, callback);
}
scheduleWork(root);
@@ -129,24 +133,25 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// It may seem strange that we don't return the root here, but that will
// allow us to have containers that are in the middle of the tree instead
// of being roots.
return container;
return current;
},
updateContainer(element : ReactElement<any>, container : OpaqueNode, parentComponent : ?ReactComponent<any, any, any>, callback: ?Function) : void {
// TODO: If this is a nested container, this won't be the root.
const root : FiberRoot = (container.stateNode : any);
if (callback) {
const queue = root.callbackList ?
root.callbackList :
createUpdateQueue(null);
addCallbackToQueue(queue, callback);
root.callbackList = queue;
}
const current = root.current;
root.pendingContext = getContextForSubtree(parentComponent);
// TODO: Use pending work/state instead of props.
root.current.pendingProps = element;
if (root.current.alternate) {
root.current.alternate.pendingProps = element;
// TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps.
// TODO: This should not override the pendingWorkPriority if there is
// higher priority work in the subtree.
current.pendingProps = element;
if (current.alternate) {
current.alternate.pendingProps = element;
}
if (callback) {
scheduleUpdateCallback(current, callback);
}
scheduleWork(root);
@@ -178,6 +183,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
syncUpdates,
deferredUpdates,
getPublicRootInstance(container : OpaqueNode) : (ReactComponent<any, any, any> | I | TI | null) {
const root : FiberRoot = (container.stateNode : any);
const containerFiber = root.current;
@@ -13,7 +13,6 @@
'use strict';
import type { Fiber } from 'ReactFiber';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
const { createHostRootFiber } = require('ReactFiber');
@@ -26,8 +25,6 @@ export type FiberRoot = {
isScheduled: boolean,
// The work schedule is a linked list.
nextScheduledRoot: ?FiberRoot,
// Linked list of callbacks to call after updates are committed.
callbackList: ?UpdateQueue,
// Top context object, used by renderSubtreeIntoContainer
context: Object,
pendingContext: ?Object,
@@ -53,6 +53,14 @@ var {
ClassComponent,
} = require('ReactTypeOfWork');
var {
getPendingPriority,
addUpdate,
addReplaceUpdate,
addForceUpdate,
addCallback,
} = require('ReactFiberUpdateQueue');
var {
unwindContext,
} = require('ReactFiberContext');
@@ -67,8 +75,14 @@ var timeHeuristicForUnitOfWork = 1;
module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C, CX>) {
const hostContext = ReactFiberHostContext(config);
const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
const { beginWork, beginFailedWork } =
ReactFiberBeginWork(config, hostContext, scheduleUpdate);
const { beginWork, beginFailedWork } = ReactFiberBeginWork(
config,
hostContext,
scheduleSetState,
scheduleReplaceState,
scheduleForceUpdate,
scheduleUpdateCallback,
);
const { completeWork } = ReactFiberCompleteWork(config, hostContext);
const {
commitPlacement,
@@ -85,10 +99,15 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
} = config;
// The priority level to use when scheduling an update.
// TODO: Should we change this to an array? Might be less confusing.
let priorityContext : PriorityLevel = useSyncScheduling ?
SynchronousPriority :
LowPriority;
// Keep track of this so we can reset the priority context if an error
// is thrown during reconciliation.
let priorityContextBeforeReconciliation : PriorityLevel = NoWork;
// Keeps track of whether we're currently in a work loop. Used to batch
// nested updates.
let isPerformingWork : boolean = false;
@@ -139,6 +158,9 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
}
// findNextUnitOfWork mutates the current priority context. It is reset after
// after the workLoop exits, so never call findNextUnitOfWork from outside
// the work loop.
function findNextUnitOfWork() {
// Clear out roots with no more work on them, or if they have uncaught errors
while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
@@ -175,6 +197,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
if (highestPriorityRoot) {
nextPriorityLevel = highestPriorityLevel;
priorityContext = nextPriorityLevel;
return cloneFiber(
highestPriorityRoot.current,
highestPriorityLevel
@@ -199,7 +222,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
let primaryEffectTag = nextEffect.effectTag & ~(Callback | Err | ContentReset);
let primaryEffectTag =
nextEffect.effectTag & ~(Callback | Err | ContentReset);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
@@ -355,7 +379,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// If we caught any errors during this commit, schedule their boundaries
// to update.
if (commitPhaseBoundaries) {
commitPhaseBoundaries.forEach(scheduleUpdate);
commitPhaseBoundaries.forEach(scheduleErrorRecovery);
commitPhaseBoundaries = null;
}
@@ -364,6 +388,14 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
function resetWorkPriority(workInProgress : Fiber) {
let newPriority = NoWork;
// Check for pending update priority. This is usually null so it shouldn't
// be a perf issue.
const queue = workInProgress.updateQueue;
if (queue) {
newPriority = getPendingPriority(queue);
}
// progressedChild is going to be the child set with the highest priority.
// Either it is the same as child, or it just bailed out because it choose
// not to do the work.
@@ -392,7 +424,6 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// The work is now done. We don't need this anymore. This flags
// to the system not to redo any work here.
workInProgress.pendingProps = null;
workInProgress.updateQueue = null;
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
@@ -459,6 +490,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
@@ -491,10 +523,13 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
ReactDebugCurrentFiber.current = null;
}
return next;
}
function performFailedUnitOfWork(workInProgress : Fiber) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
@@ -642,28 +677,25 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
// Before starting any work, check to see if there are any pending
// commits from the previous frame. An exception is if we're flushing
// Task work in a deferred batch and the pending commit does not
// have Task priority.
if (pendingCommit) {
const isFlushingTaskWorkInDeferredBatch =
priorityLevel === TaskPriority &&
isPerformingDeferredWork &&
pendingCommit.pendingWorkPriority !== TaskPriority;
if (!isFlushingTaskWorkInDeferredBatch) {
commitAllWork(pendingCommit);
}
// commits from the previous frame.
if (pendingCommit && !deadlineHasExpired) {
commitAllWork(pendingCommit);
}
// Nothing in performWork should be allowed to throw. All unsafe
// operations must happen within workLoop, which is extracted to a
// separate function so that it can be optimized by the JS engine.
try {
priorityContextBeforeReconciliation = priorityContext;
priorityContext = nextPriorityLevel;
deadlineHasExpired = workLoop(priorityLevel, deadline, deadlineHasExpired);
} catch (error) {
// We caught an error during either the begin or complete phases.
const failedWork = nextUnitOfWork;
// Reset the priority context to its value before reconcilation.
priorityContext = priorityContextBeforeReconciliation;
// "Capture" the error by finding the nearest boundary. If there is no
// error boundary, the nearest host container acts as one. If
// captureError returns null, the error was intentionally ignored.
@@ -691,6 +723,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
// Continue performing work
continue;
} finally {
priorityContext = priorityContextBeforeReconciliation;
}
// Stop performing work
@@ -828,11 +862,8 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
commitPhaseBoundaries.add(boundary);
} else {
// Otherwise, schedule an update now. Error recovery has Task priority.
const previousPriorityContext = priorityContext;
priorityContext = TaskPriority;
scheduleUpdate(boundary);
priorityContext = previousPriorityContext;
// Otherwise, schedule an update now.
scheduleErrorRecovery(boundary);
}
return boundary;
} else if (!firstUncaughtError) {
@@ -982,8 +1013,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
}
function scheduleUpdate(fiber : Fiber) {
let priorityLevel = priorityContext;
function scheduleUpdateAtPriority(fiber : Fiber, priorityLevel : PriorityLevel) {
// If we're in a batch, downgrade sync priority to task priority
if (priorityLevel === SynchronousPriority && isPerformingWork) {
priorityLevel = TaskPriority;
@@ -1023,6 +1053,30 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
}
function scheduleErrorRecovery(fiber : Fiber) {
scheduleUpdateAtPriority(fiber, TaskPriority);
}
function scheduleSetState(fiber : Fiber, partialState : any) {
addUpdate(fiber, partialState, priorityContext);
scheduleUpdateAtPriority(fiber, priorityContext);
}
function scheduleReplaceState(fiber : Fiber, state : any) {
addReplaceUpdate(fiber, state, priorityContext);
scheduleUpdateAtPriority(fiber, priorityContext);
}
function scheduleForceUpdate(fiber : Fiber) {
addForceUpdate(fiber, priorityContext);
scheduleUpdateAtPriority(fiber, priorityContext);
}
function scheduleUpdateCallback(fiber : Fiber, callback : Function) {
addCallback(fiber, callback, priorityContext);
scheduleUpdateAtPriority(fiber, priorityContext);
}
function performWithPriority(priorityLevel : PriorityLevel, fn : Function) {
const previousPriorityContext = priorityContext;
priorityContext = priorityLevel;
@@ -1059,10 +1113,22 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
}
function deferredUpdates<A>(fn : () => A) : A {
const previousPriorityContext = priorityContext;
priorityContext = LowPriority;
try {
return fn();
} finally {
priorityContext = previousPriorityContext;
}
}
return {
scheduleWork: scheduleWork,
scheduleUpdateCallback: scheduleUpdateCallback,
performWithPriority: performWithPriority,
batchedUpdates: batchedUpdates,
syncUpdates: syncUpdates,
deferredUpdates: deferredUpdates,
};
};
@@ -12,100 +12,479 @@
'use strict';
type UpdateQueueNode = {
partialState: any,
callback: ?Function,
import type { Fiber } from 'ReactFiber';
import type { PriorityLevel } from 'ReactPriorityLevel';
const {
Callback: CallbackEffect,
} = require('ReactTypeOfSideEffect');
const {
NoWork,
SynchronousPriority,
TaskPriority,
} = require('ReactPriorityLevel');
type PartialState<State, Props> =
$Subtype<State> |
(prevState: State, props: Props) => $Subtype<State>;
type Callback = () => void;
type Update = {
priorityLevel: PriorityLevel,
partialState: PartialState<any, any>,
callback: Callback | null,
isReplace: boolean,
next: ?UpdateQueueNode,
};
export type UpdateQueue = UpdateQueueNode & {
isForced: boolean,
hasUpdate: boolean,
hasCallback: boolean,
tail: UpdateQueueNode
next: Update | null,
};
exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue {
const queue = {
partialState,
callback: null,
isReplace: false,
next: null,
isForced: false,
hasUpdate: partialState != null,
hasCallback: false,
tail: (null : any),
};
queue.tail = queue;
return queue;
// Singly linked-list of updates. When an update is scheduled, it is added to
// the queue of the current fiber and the work-in-progress fiber. The two queues
// are separate but they share a persistent structure.
//
// During reconciliation, updates are removed from the work-in-progress fiber,
// but they remain on the current fiber. That ensures that if a work-in-progress
// is aborted, the aborted updates are recovered by cloning from current.
//
// 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,
hasForceUpdate: boolean,
// Dev only
isProcessing?: boolean,
};
function addToQueue(queue : UpdateQueue, partialState : mixed) : UpdateQueue {
const node = {
partialState,
callback: null,
isReplace: false,
next: null,
};
queue.tail.next = node;
queue.tail = node;
queue.hasUpdate = queue.hasUpdate || (partialState != null);
function comparePriority(a : PriorityLevel, b : PriorityLevel) : number {
// When comparing update priorities, treat sync and Task work as equal.
// TODO: Could we avoid the need for this by always coercing sync priority
// to Task when scheduling an update?
if ((a === TaskPriority || a === SynchronousPriority) &&
(b === TaskPriority || b === SynchronousPriority)) {
return 0;
}
if (a === NoWork && b !== NoWork) {
return -255;
}
if (a !== NoWork && b === NoWork) {
return 255;
}
return a - b;
}
function hasPendingUpdate(queue : UpdateQueue, priorityLevel : PriorityLevel) : boolean {
if (!queue.first) {
return false;
}
// Return true if the first pending update has greater or equal priority.
return comparePriority(queue.first.priorityLevel, priorityLevel) <= 0;
}
exports.hasPendingUpdate = hasPendingUpdate;
// Ensures that a fiber has an update queue, creating a new one if needed.
// Returns the new or existing queue.
function ensureUpdateQueue(fiber : Fiber) : UpdateQueue {
if (fiber.updateQueue) {
// We already have an update queue.
return fiber.updateQueue;
}
let queue;
if (__DEV__) {
queue = {
first: null,
last: null,
hasForceUpdate: false,
isProcessing: false,
};
} else {
queue = {
first: null,
last: null,
hasForceUpdate: false,
};
}
fiber.updateQueue = queue;
return queue;
}
exports.addToQueue = addToQueue;
exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) : UpdateQueue {
if (queue.tail.callback) {
// If the tail already as a callback, add an empty node to queue
addToQueue(queue, null);
// Clones an update queue from a source fiber onto its alternate.
function cloneUpdateQueue(alt : Fiber, fiber : Fiber) : UpdateQueue | null {
const sourceQueue = fiber.updateQueue;
if (!sourceQueue) {
// The source fiber does not have an update queue.
alt.updateQueue = null;
return null;
}
queue.tail.callback = callback;
queue.hasCallback = true;
return queue;
};
// If the alternate already has a queue, reuse the previous object.
const altQueue = alt.updateQueue || {};
altQueue.first = sourceQueue.first;
altQueue.last = sourceQueue.last;
altQueue.hasForceUpdate = sourceQueue.hasForceUpdate;
alt.updateQueue = altQueue;
return altQueue;
}
exports.cloneUpdateQueue = cloneUpdateQueue;
exports.callCallbacks = function(queue : UpdateQueue, context : any) {
let node : ?UpdateQueueNode = queue;
while (node) {
const callback = node.callback;
if (callback) {
if (typeof context !== 'undefined') {
callback.call(context);
} else {
callback();
function cloneUpdate(update : Update) : Update {
return {
priorityLevel: update.priorityLevel,
partialState: update.partialState,
callback: update.callback,
isReplace: update.isReplace,
isForced: update.isForced,
next: null,
};
}
function insertUpdateIntoQueue(queue, update, insertAfter, insertBefore) {
if (insertAfter) {
insertAfter.next = update;
} else {
// This is the first item in the queue.
update.next = queue.first;
queue.first = update;
}
if (insertBefore) {
update.next = insertBefore;
} else {
// This is the last item in the queue.
queue.last = update;
}
}
// 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 stucture, 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.
function insertUpdate(fiber : Fiber, update : Update, methodName : ?string) : void {
const queue1 = ensureUpdateQueue(fiber);
const queue2 = fiber.alternate ? ensureUpdateQueue(fiber.alternate) : null;
// Warn if an update is scheduled from inside an updater function.
if (__DEV__ && typeof methodName === 'string' && (queue1.isProcessing || (queue2 && queue2.isProcessing))) {
if (methodName === 'setState') {
console.error(
'setState was called from inside the updater function of another' +
'setState. A function passed as the first argument of setState ' +
'should not contain any side-effects. Return a partial state object ' +
'instead of calling setState again. Example: ' +
'this.setState(function(state) { return { count: state.count + 1 }; })'
);
} else {
console.error(
`${methodName} was called from inside the updater function of ` +
'setState. A function passed as the first argument of setState ' +
'should not contain any side-effects.'
);
}
}
const priorityLevel = update.priorityLevel;
let queue = queue1;
let insertAfter1;
let insertBefore1;
let insertAfter2;
let insertBefore2;
for (let i = 0; queue && i < 2; i++) {
let insertAfter = null;
let insertBefore = null;
if (queue.last && comparePriority(queue.last.priorityLevel, priorityLevel) <= 0) {
// 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 && comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0) {
insertAfter = insertBefore;
insertBefore = insertBefore.next;
}
}
node = node.next;
if (i === 0) {
insertAfter1 = insertAfter;
insertBefore1 = insertBefore;
queue = queue2;
} else {
insertAfter2 = insertAfter;
insertBefore2 = insertBefore;
queue = null;
}
}
};
function getStateFromNode(node, instance, state, props) {
if (typeof node.partialState === 'function') {
const updateFn = node.partialState;
return updateFn.call(instance, state, props);
} else {
return node.partialState;
const update1 = update;
insertUpdateIntoQueue(queue1, update1, insertAfter1, insertBefore1);
if (queue2) {
let update2;
if (insertBefore1 === insertBefore2) {
// The update is inserted into the same position of both lists. There's no
// need to clone the update.
update2 = update1;
} else {
// The update is inserted into two separate positions. Make a clone of the
// update and insert it twice. One or the other will be dropped the next
// time we commit.
update2 = cloneUpdate(update1);
}
insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
}
}
exports.mergeUpdateQueue = function(queue : UpdateQueue, instance : any, prevState : any, props : any) : any {
let node : ?UpdateQueueNode = queue;
if (queue.isReplace) {
// replaceState is always first in the queue.
prevState = getStateFromNode(queue, instance, prevState, props);
node = queue.next;
if (!node) {
// If there is no more work, we replace the raw object instead of cloning.
return prevState;
function addUpdate(
fiber : Fiber,
partialState : PartialState<any, any> | null,
priorityLevel : PriorityLevel
) : void {
const update = {
priorityLevel,
partialState,
callback: null,
isReplace: false,
isForced: false,
next: null,
};
if (__DEV__) {
insertUpdate(fiber, update, 'setState');
} else {
insertUpdate(fiber, update);
}
}
exports.addUpdate = addUpdate;
function addReplaceUpdate(
fiber : Fiber,
state : any | null,
priorityLevel : PriorityLevel
) : void {
const update = {
priorityLevel,
partialState: state,
callback: null,
isReplace: true,
isForced: false,
next: null,
};
// Drop all updates with equal priority
let queue = ensureUpdateQueue(fiber);
for (let i = 0; queue && i < 2; i++) {
let replaceAfter = null;
let replaceBefore = queue.first;
let comparison = 255;
while (replaceBefore &&
(comparison = comparePriority(replaceBefore.priorityLevel, priorityLevel)) <= 0) {
if (comparison < 0) {
replaceAfter = replaceBefore;
}
replaceBefore = replaceBefore.next;
}
if (replaceAfter) {
replaceAfter.next = replaceBefore;
} else {
queue.first = replaceBefore;
}
if (!replaceBefore) {
queue.last = replaceAfter;
}
if (fiber.alternate) {
queue = ensureUpdateQueue(fiber.alternate);
} else {
queue = null;
}
}
let state = Object.assign({}, prevState);
while (node) {
let partialState = getStateFromNode(node, instance, state, props);
Object.assign(state, partialState);
node = node.next;
if (__DEV__) {
insertUpdate(fiber, update, 'replaceState');
} else {
insertUpdate(fiber, update);
}
}
exports.addReplaceUpdate = addReplaceUpdate;
function addForceUpdate(fiber : Fiber, priorityLevel : PriorityLevel) : void {
const update = {
priorityLevel,
partialState: null,
callback: null,
isReplace: false,
isForced: true,
next: null,
};
if (__DEV__) {
insertUpdate(fiber, update, 'forceUpdate');
} else {
insertUpdate(fiber, update);
}
}
exports.addForceUpdate = addForceUpdate;
function addCallback(fiber : Fiber, callback: Callback, priorityLevel : PriorityLevel) : void {
const update : Update = {
priorityLevel,
partialState: null,
callback,
isReplace: false,
isForced: false,
next: null,
};
insertUpdate(fiber, update);
}
exports.addCallback = addCallback;
function getPendingPriority(queue : UpdateQueue) : PriorityLevel {
return queue.first ? queue.first.priorityLevel : NoWork;
}
exports.getPendingPriority = getPendingPriority;
function getStateFromUpdate(update, instance, prevState, props) {
const partialState = update.partialState;
if (typeof partialState === 'function') {
const updateFn = partialState;
return updateFn.call(instance, prevState, props);
} else {
return partialState;
}
}
function beginUpdateQueue(
workInProgress : Fiber,
queue : UpdateQueue,
instance : any,
prevState : any,
props : any,
priorityLevel : PriorityLevel
) : any {
if (__DEV__) {
// Set this flag so we can warn if setState is called inside the update
// function of another setState.
queue.isProcessing = true;
}
queue.hasForceUpdate = false;
// Applies updates with matching priority to the previous state to create
// a new state object.
let state = prevState;
let dontMutatePrevState = true;
let isEmpty = true;
let callbackList = null;
let update = queue.first;
while (update && comparePriority(update.priorityLevel, priorityLevel) <= 0) {
// 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) {
queue.last = null;
}
let partialState;
if (update.isReplace) {
// A replace should drop all previous updates in the queue, so
// use the original `prevState`, not the accumulated `state`
state = getStateFromUpdate(update, instance, prevState, props);
dontMutatePrevState = true;
isEmpty = false;
} else {
partialState = getStateFromUpdate(update, instance, state, props);
if (partialState) {
if (dontMutatePrevState) {
state = Object.assign({}, state, partialState);
} else {
state = Object.assign(state, partialState);
}
dontMutatePrevState = false;
isEmpty = false;
}
}
if (update.isForced) {
queue.hasForceUpdate = true;
}
if (update.callback) {
if (callbackList && callbackList.last) {
callbackList.last.next = update;
callbackList.last = update;
} else {
callbackList = {
first: update,
last: update,
hasForceUpdate: false,
};
}
workInProgress.effectTag |= CallbackEffect;
}
update = update.next;
}
if (isEmpty) {
// None of the updates contained state. Use the original state object.
state = prevState;
}
if (!queue.first && !queue.hasForceUpdate) {
// Queue is now empty
workInProgress.updateQueue = null;
}
workInProgress.callbackList = callbackList;
workInProgress.memoizedState = state;
if (__DEV__) {
queue.isProcessing = false;
}
return state;
};
}
exports.beginUpdateQueue = beginUpdateQueue;
function commitCallbacks(finishedWork : Fiber, callbackList : UpdateQueue, context : mixed) {
const stopAfter = callbackList.last;
let update = callbackList.first;
while (update) {
const callback = update.callback;
if (typeof callback === 'function') {
callback.call(context);
}
if (update === stopAfter) {
break;
}
update = update.next;
}
finishedWork.callbackList = null;
}
exports.commitCallbacks = commitCallbacks;
@@ -0,0 +1,354 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var React;
var ReactNoop;
describe('ReactIncrementalUpdates', () => {
beforeEach(() => {
jest.resetModuleRegistry();
React = require('React');
ReactNoop = require('ReactNoop');
});
it('applies updates in order of priority', () => {
let state;
class Foo extends React.Component {
state = {};
componentDidMount() {
ReactNoop.performAnimationWork(() => {
// Has Animation priority
this.setState({ b: 'b' });
this.setState({ c: 'c' });
});
// Has Task priority
this.setState({ a: 'a' });
}
render() {
state = this.state;
return <div />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flushDeferredPri(25);
expect(state).toEqual({ a: 'a' });
ReactNoop.flush();
expect(state).toEqual({ a: 'a', b: 'b', c: 'c' });
});
it('applies updates with equal priority in insertion order', () => {
let state;
class Foo extends React.Component {
state = {};
componentDidMount() {
// All have Task priority
this.setState({ a: 'a' });
this.setState({ b: 'b' });
this.setState({ c: 'c' });
}
render() {
state = this.state;
return <div />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(state).toEqual({ a: 'a', b: 'b', c: 'c' });
});
it('only drops updates with equal or lesser priority when replaceState is called', () => {
let instance;
let ops = [];
const Foo = React.createClass({
getInitialState() {
return {};
},
componentDidMount() {
ops.push('componentDidMount');
},
componentDidUpdate() {
ops.push('componentDidUpdate');
},
render() {
ops.push('render');
instance = this;
return <div />;
},
});
ReactNoop.render(<Foo />);
ReactNoop.flush();
instance.setState({ x: 'x' });
instance.setState({ y: 'y' });
ReactNoop.performAnimationWork(() => {
instance.setState({ a: 'a' });
instance.setState({ b: 'b' });
});
instance.replaceState({ c: 'c' });
instance.setState({ d: 'd' });
ReactNoop.flushAnimationPri();
// Even though a replaceState has been already scheduled, it hasn't been
// flushed yet because it has low priority.
expect(instance.state).toEqual({ a: 'a', b: 'b' });
expect(ops).toEqual([
'render',
'componentDidMount',
'render',
'componentDidUpdate',
]);
ops = [];
ReactNoop.flush();
// Now the rest of the updates are flushed.
expect(instance.state).toEqual({ c: 'c', d: 'd' });
expect(ops).toEqual([
'render',
'componentDidUpdate',
]);
});
it('can abort an update, schedule additional updates, and resume', () => {
let instance;
let ops = [];
class Foo extends React.Component {
state = {};
componentDidUpdate() {
ops.push('componentDidUpdate');
}
render() {
ops.push('render');
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
ops = [];
let progressedUpdates = [];
function createUpdate(letter) {
return () => {
progressedUpdates.push(letter);
return {
[letter]: letter,
};
};
}
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
// Do just enough work to begin the update but not enough to flush it
ReactNoop.flushDeferredPri(15);
// expect(ReactNoop.getChildren()).toEqual([span('')]);
expect(ops).toEqual(['render']);
expect(progressedUpdates).toEqual(['a', 'b', 'c']);
expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c' });
ops = [];
progressedUpdates = [];
instance.setState(createUpdate('f'));
ReactNoop.performAnimationWork(() => {
instance.setState(createUpdate('d'));
instance.setState(createUpdate('e'));
});
instance.setState(createUpdate('g'));
ReactNoop.flushAnimationPri();
expect(ops).toEqual([
// Flushes animation work (d and e)
'render',
'componentDidUpdate',
]);
ops = [];
ReactNoop.flush();
expect(ops).toEqual([
// Flushes deferred work (f and g)
'render',
'componentDidUpdate',
]);
expect(progressedUpdates).toEqual(['d', 'e', 'a', 'b', 'c', 'f', 'g']);
expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e', f: 'f', g: 'g' });
});
it('can abort an update, schedule a replaceState, and resume', () => {
let instance;
let ops = [];
const Foo = React.createClass({
getInitialState() {
return {};
},
componentDidUpdate() {
ops.push('componentDidUpdate');
},
render() {
ops.push('render');
instance = this;
return (
<span prop={Object.keys(this.state).join('')} />
);
},
});
ReactNoop.render(<Foo />);
ReactNoop.flush();
ops = [];
let progressedUpdates = [];
function createUpdate(letter) {
return () => {
progressedUpdates.push(letter);
return {
[letter]: letter,
};
};
}
instance.setState(createUpdate('a'));
instance.setState(createUpdate('b'));
instance.setState(createUpdate('c'));
// Do just enough work to begin the update but not enough to flush it
ReactNoop.flushDeferredPri(20);
expect(ops).toEqual(['render']);
expect(progressedUpdates).toEqual(['a', 'b', 'c']);
expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c' });
ops = [];
progressedUpdates = [];
instance.setState(createUpdate('f'));
ReactNoop.performAnimationWork(() => {
instance.setState(createUpdate('d'));
instance.replaceState(createUpdate('e'));
});
instance.setState(createUpdate('g'));
ReactNoop.flush();
// Ensure that updater function d is never called.
expect(progressedUpdates).toEqual(['e', 'f', 'g']);
expect(instance.state).toEqual({ e: 'e', f: 'f', g: 'g' });
});
it('does not call callbacks that are scheduled by another callback until a later commit', () => {
let ops = [];
class Foo extends React.Component {
state = {};
componentDidMount() {
ops.push('did mount');
this.setState({ a: 'a' }, () => {
ops.push('callback a');
this.setState({ b: 'b' }, () => {
ops.push('callback b');
});
});
}
render() {
ops.push('render');
return <div />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ops).toEqual([
'render',
'did mount',
'render',
'callback a',
'render',
'callback b',
]);
});
it('gives setState during reconciliation the same priority as whatever level is currently reconciling', () => {
let instance;
let ops = [];
class Foo extends React.Component {
state = {};
componentWillReceiveProps() {
ops.push('componentWillReceiveProps');
this.setState({ b: 'b' });
}
render() {
ops.push('render');
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
ops = [];
ReactNoop.performAnimationWork(() => {
instance.setState({ a: 'a' });
ReactNoop.render(<Foo />); // Trigger componentWillReceiveProps
});
ReactNoop.flush();
expect(instance.state).toEqual({ a: 'a', b: 'b' });
expect(ops).toEqual([
'componentWillReceiveProps',
'render',
]);
});
it('enqueues setState inside an updater function as if the in-progress update is progressed (and warns)', () => {
spyOn(console, 'error');
let instance;
let ops = [];
class Foo extends React.Component {
state = {};
render() {
ops.push('render');
instance = this;
return <div />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
instance.setState(function a() {
ops.push('setState updater');
this.setState({ b: 'b' });
return { a: 'a' };
});
ReactNoop.flush();
expect(ops).toEqual([
// Initial render
'render',
'setState updater',
// Update b is enqueued with the same priority as update a, so it should
// be flushed in the same commit.
'render',
]);
expect(instance.state).toEqual({ a: 'a', b: 'b' });
expectDev(console.error.calls.count()).toBe(1);
console.error.calls.reset();
});
});