Priority context during reconciliation

setState inside render/cWRP should have the same priority as whatever
level is currently being reconciled.
This commit is contained in:
Andrew Clark
2016-12-15 09:12:13 -08:00
parent e0981b8bc5
commit eec431297a
6 changed files with 125 additions and 78 deletions
+1
View File
@@ -1237,6 +1237,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
* 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
@@ -71,8 +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>,
scheduleUpdateAtPriority : (fiber: Fiber, priorityLevel : PriorityLevel) => void,
getPriorityContext : () => PriorityLevel
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;
@@ -89,7 +91,12 @@ module.exports = function<T, P, I, TI, C, CX>(
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
} = ReactFiberClassComponent(scheduleUpdateAtPriority, getPriorityContext);
} = ReactFiberClassComponent(
scheduleSetState,
scheduleReplaceState,
scheduleForceUpdate,
scheduleUpdateCallback
);
function markChildAsProgressed(current, workInProgress, priorityLevel) {
// We now have clones. Let's store them as the currently progressed work.
@@ -19,10 +19,6 @@ var {
getMaskedContext,
} = require('ReactFiberContext');
var {
addUpdate,
addReplaceUpdate,
addForceUpdate,
addCallback,
beginUpdateQueue,
} = require('ReactFiberUpdateQueue');
var { hasContextChanged } = require('ReactFiberContext');
@@ -36,8 +32,10 @@ var invariant = require('invariant');
const isArray = Array.isArray;
module.exports = function(
scheduleUpdateAtPriority : (fiber: Fiber, priorityLevel : PriorityLevel) => void,
getPriorityContext : () => PriorityLevel,
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
@@ -45,27 +43,19 @@ module.exports = function(
isMounted,
enqueueSetState(instance, partialState) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext();
addUpdate(fiber, partialState, priorityLevel);
scheduleUpdateAtPriority(fiber, priorityLevel);
scheduleSetState(fiber, partialState);
},
enqueueReplaceState(instance, state) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext();
addReplaceUpdate(fiber, state, priorityLevel);
scheduleUpdateAtPriority(fiber, priorityLevel);
scheduleReplaceState(fiber, state);
},
enqueueForceUpdate(instance) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext();
addForceUpdate(fiber, priorityLevel);
scheduleUpdateAtPriority(fiber, priorityLevel);
scheduleForceUpdate(fiber);
},
enqueueCallback(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext();
addCallback(fiber, callback, priorityLevel);
scheduleUpdateAtPriority(fiber, priorityLevel);
scheduleUpdateCallback(fiber, callback);
},
};
@@ -24,8 +24,6 @@ var {
var { createFiberRoot } = require('ReactFiberRoot');
var ReactFiberScheduler = require('ReactFiberScheduler');
var { addCallback } = require('ReactFiberUpdateQueue');
if (__DEV__) {
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
}
@@ -100,7 +98,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
var {
scheduleWork,
getPriorityContext,
scheduleUpdateCallback,
performWithPriority,
batchedUpdates,
syncUpdates,
@@ -123,8 +121,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
current.alternate.pendingProps = element;
}
if (callback) {
const priorityLevel = getPriorityContext();
addCallback(current, callback, priorityLevel);
scheduleUpdateCallback(current, callback);
}
scheduleWork(root);
@@ -154,8 +151,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
current.alternate.pendingProps = element;
}
if (callback) {
const priorityLevel = getPriorityContext();
addCallback(current, callback, priorityLevel);
scheduleUpdateCallback(current, callback);
}
scheduleWork(root);
@@ -56,6 +56,10 @@ var {
var {
getPendingPriority,
addUpdate,
addReplaceUpdate,
addForceUpdate,
addCallback,
} = require('ReactFiberUpdateQueue');
var {
@@ -72,8 +76,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, scheduleUpdateAtPriority, getPriorityContext);
const { beginWork, beginFailedWork } = ReactFiberBeginWork(
config,
hostContext,
scheduleSetState,
scheduleReplaceState,
scheduleForceUpdate,
scheduleUpdateCallback,
);
const { completeWork } = ReactFiberCompleteWork(config, hostContext);
const {
commitPlacement,
@@ -95,6 +105,10 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
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;
@@ -145,15 +159,6 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
}
function getPriorityContext() : PriorityLevel {
let priorityLevel = priorityContext;
// If we're in a batch, downgrade sync priority to task priority
if (priorityLevel === SynchronousPriority && isPerformingWork) {
priorityLevel = TaskPriority;
}
return priorityLevel;
}
function findNextUnitOfWork() {
// Clear out roots with no more work on them, or if they have uncaught errors
while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
@@ -481,6 +486,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,7 +497,11 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress);
}
// See if beginning this work spawns more work.
priorityContextBeforeReconciliation = priorityContext;
priorityContext = nextPriorityLevel;
let next = beginWork(current, workInProgress, nextPriorityLevel);
priorityContext = priorityContextBeforeReconciliation;
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress);
@@ -513,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
@@ -527,7 +540,10 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress);
}
// See if beginning this work spawns more work.
priorityContextBeforeReconciliation = priorityContext;
priorityContext = nextPriorityLevel;
let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
priorityContext = priorityContextBeforeReconciliation;
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress);
@@ -686,6 +702,9 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
// 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.
@@ -1002,6 +1021,11 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
function scheduleUpdateAtPriority(fiber : Fiber, priorityLevel : PriorityLevel) {
// If we're in a batch, downgrade sync priority to task priority
if (priorityLevel === SynchronousPriority && isPerformingWork) {
priorityLevel = TaskPriority;
}
let node = fiber;
let shouldContinue = true;
while (node && shouldContinue) {
@@ -1040,6 +1064,26 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
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;
@@ -1088,7 +1132,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
return {
scheduleWork: scheduleWork,
getPriorityContext: getPriorityContext,
scheduleUpdateCallback: scheduleUpdateCallback,
performWithPriority: performWithPriority,
batchedUpdates: batchedUpdates,
syncUpdates: syncUpdates,
@@ -246,22 +246,47 @@ describe('ReactIncrementalUpdates', () => {
]);
});
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 <span prop={Object.keys(this.state).join('')} />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
ops = [];
ReactNoop.performAnimationWork(() => {
instance.setState({ a: 'a' });
ReactNoop.render(<Foo />); // Trigger componentWillReceiveProps
});
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('ab')]);
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 = {};
componentDidMount() {
ops.push('componentDidMount');
this.setState(function a() {
// Force update b to have Task priority
ReactNoop.syncUpdates(() => {
this.setState({ b: 'b' });
});
return { a: 'a' };
});
}
render() {
ops.push('render');
instance = this;
@@ -271,41 +296,25 @@ describe('ReactIncrementalUpdates', () => {
ReactNoop.render(<Foo />);
ReactNoop.flush();
expectDev(console.error.calls.count()).toBe(1);
instance.setState(function a() {
ops.push('setState updater');
this.setState({ b: 'b' });
return { a: 'a' };
});
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('ab')]);
expect(ops).toEqual([
// Initial render
'render',
'componentDidMount',
// Updates a and b both have Task priority. Update b is enqueued while
// update a is being processed, but it should be inserted into the queue
// as if update a is already processed. Then processing continues. Because
// they have the same priority, update b is processed in the same batch.
// So there should only be a single render below.
'setState updater',
// Update b is enqueued with the same priority as update a, so it should
// be flushed in the same commit.
'render',
]);
ops = [];
ReactNoop.performAnimationWork(() => {
instance.setState(function c() {
// Update d happens during the begin phase, so it has low priority.
this.setState({ d: 'd' });
return { c: 'c' };
});
});
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span('abcd')]);
expect(ops).toEqual([
// Update c has animation priority. Update d is enqueued while c is being
// processed with animation priority. Because d is low priority, it is not
// processed until the next render. So there should be two renders below.
'render',
'render',
]);
expectDev(console.error.calls.count()).toBe(2);
expectDev(console.error.calls.count()).toBe(1);
console.error.calls.reset();
});
});