mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #8538 from acdlite/fiberupdatequeue
[Fiber] Separate priority for updates
This commit is contained in:
+141
-13
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -242,6 +242,8 @@ var ReactDOM = {
|
||||
|
||||
unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
|
||||
|
||||
unstable_deferredUpdates: DOMRenderer.deferredUpdates,
|
||||
|
||||
};
|
||||
|
||||
module.exports = ReactDOM;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user