Priority context during reconciliation
setState inside render/cWRP should have the same priority as whatever level is currently being reconciled.
Andrew Clark committed
Dec 13, 2016 at 15:05 UTC
eec431297a145ccc0159ce3a1bf093e47c9c88d5
6 files changed
+120
-73
scripts/fiber/tests-passing.txt
+1
@@ -1237,6 +1237,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
1237
* can abort an update, schedule additional updates, and resume
1238
* can abort an update, schedule a replaceState, and resume
1239
* does not call callbacks that are scheduled by another callback until a later commit
1240
+* gives setState during reconciliation the same priority as whatever level is currently reconciling
1241
* enqueues setState inside an updater function as if the in-progress update is progressed (and warns)
1242
1243
src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-test.js
src/renderers/shared/fiber/ReactFiberBeginWork.js
+10
-3
@@ -71,8 +71,10 @@ if (__DEV__) {
71
module.exports = function<T, P, I, TI, C, CX>(
72
config : HostConfig<T, P, I, TI, C, CX>,
73
hostContext : HostContext<C, CX>,
74
- scheduleUpdateAtPriority : (fiber: Fiber, priorityLevel : PriorityLevel) => void,
75
- getPriorityContext : () => PriorityLevel
74
+ scheduleSetState: (fiber : Fiber, partialState : any) => void,
75
+ scheduleReplaceState: (fiber : Fiber, state : any) => void,
76
+ scheduleForceUpdate: (fiber : Fiber) => void,
77
+ scheduleUpdateCallback: (fiber : Fiber, callback : Function) => void,
78
) {
79
80
const { shouldSetTextContent } = config;
@@ -89,7 +91,12 @@ module.exports = function<T, P, I, TI, C, CX>(
91
mountClassInstance,
92
resumeMountClassInstance,
93
updateClassInstance,
92
- } = ReactFiberClassComponent(scheduleUpdateAtPriority, getPriorityContext);
94
+ } = ReactFiberClassComponent(
95
+ scheduleSetState,
96
+ scheduleReplaceState,
97
+ scheduleForceUpdate,
98
+ scheduleUpdateCallback
99
+ );
100
101
function markChildAsProgressed(current, workInProgress, priorityLevel) {
102
// We now have clones. Let's store them as the currently progressed work.
src/renderers/shared/fiber/ReactFiberClassComponent.js
+8
-18
@@ -19,10 +19,6 @@ var {
19
getMaskedContext,
20
} = require('ReactFiberContext');
21
var {
22
- addUpdate,
23
- addReplaceUpdate,
24
- addForceUpdate,
25
- addCallback,
22
beginUpdateQueue,
23
} = require('ReactFiberUpdateQueue');
24
var { hasContextChanged } = require('ReactFiberContext');
@@ -36,8 +32,10 @@ var invariant = require('invariant');
32
const isArray = Array.isArray;
33
34
module.exports = function(
39
- scheduleUpdateAtPriority : (fiber: Fiber, priorityLevel : PriorityLevel) => void,
40
- getPriorityContext : () => PriorityLevel,
35
+ scheduleSetState: (fiber : Fiber, partialState : any) => void,
36
+ scheduleReplaceState: (fiber : Fiber, state : any) => void,
37
+ scheduleForceUpdate: (fiber : Fiber) => void,
38
+ scheduleUpdateCallback: (fiber : Fiber, callback : Function) => void,
39
) {
40
41
// Class component state updater
@@ -45,27 +43,19 @@ module.exports = function(
43
isMounted,
44
enqueueSetState(instance, partialState) {
45
const fiber = ReactInstanceMap.get(instance);
48
- const priorityLevel = getPriorityContext();
49
- addUpdate(fiber, partialState, priorityLevel);
50
- scheduleUpdateAtPriority(fiber, priorityLevel);
46
+ scheduleSetState(fiber, partialState);
47
},
48
enqueueReplaceState(instance, state) {
49
const fiber = ReactInstanceMap.get(instance);
54
- const priorityLevel = getPriorityContext();
55
- addReplaceUpdate(fiber, state, priorityLevel);
56
- scheduleUpdateAtPriority(fiber, priorityLevel);
50
+ scheduleReplaceState(fiber, state);
51
},
52
enqueueForceUpdate(instance) {
53
const fiber = ReactInstanceMap.get(instance);
60
- const priorityLevel = getPriorityContext();
61
- addForceUpdate(fiber, priorityLevel);
62
- scheduleUpdateAtPriority(fiber, priorityLevel);
54
+ scheduleForceUpdate(fiber);
55
},
56
enqueueCallback(instance, callback) {
57
const fiber = ReactInstanceMap.get(instance);
66
- const priorityLevel = getPriorityContext();
67
- addCallback(fiber, callback, priorityLevel);
68
- scheduleUpdateAtPriority(fiber, priorityLevel);
58
+ scheduleUpdateCallback(fiber, callback);
59
},
60
};
61
src/renderers/shared/fiber/ReactFiberReconciler.js
+3
-7
@@ -24,8 +24,6 @@ var {
24
var { createFiberRoot } = require('ReactFiberRoot');
25
var ReactFiberScheduler = require('ReactFiberScheduler');
26
27
-var { addCallback } = require('ReactFiberUpdateQueue');
28
-
27
if (__DEV__) {
28
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
29
}
@@ -100,7 +98,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
98
99
var {
100
scheduleWork,
103
- getPriorityContext,
101
+ scheduleUpdateCallback,
102
performWithPriority,
103
batchedUpdates,
104
syncUpdates,
@@ -123,8 +121,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
121
current.alternate.pendingProps = element;
122
}
123
if (callback) {
126
- const priorityLevel = getPriorityContext();
127
- addCallback(current, callback, priorityLevel);
124
+ scheduleUpdateCallback(current, callback);
125
}
126
127
scheduleWork(root);
@@ -154,8 +151,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
151
current.alternate.pendingProps = element;
152
}
153
if (callback) {
157
- const priorityLevel = getPriorityContext();
158
- addCallback(current, callback, priorityLevel);
154
+ scheduleUpdateCallback(current, callback);
155
}
156
157
scheduleWork(root);
src/renderers/shared/fiber/ReactFiberScheduler.js
+56
-12
@@ -56,6 +56,10 @@ var {
56
57
var {
58
getPendingPriority,
59
+ addUpdate,
60
+ addReplaceUpdate,
61
+ addForceUpdate,
62
+ addCallback,
63
} = require('ReactFiberUpdateQueue');
64
65
var {
@@ -72,8 +76,14 @@ var timeHeuristicForUnitOfWork = 1;
76
module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C, CX>) {
77
const hostContext = ReactFiberHostContext(config);
78
const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
75
- const { beginWork, beginFailedWork } =
76
- ReactFiberBeginWork(config, hostContext, scheduleUpdateAtPriority, getPriorityContext);
79
+ const { beginWork, beginFailedWork } = ReactFiberBeginWork(
80
+ config,
81
+ hostContext,
82
+ scheduleSetState,
83
+ scheduleReplaceState,
84
+ scheduleForceUpdate,
85
+ scheduleUpdateCallback,
86
+ );
87
const { completeWork } = ReactFiberCompleteWork(config, hostContext);
88
const {
89
commitPlacement,
@@ -95,6 +105,10 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
105
SynchronousPriority :
106
LowPriority;
107
108
+ // Keep track of this so we can reset the priority context if an error
109
+ // is thrown during reconciliation.
110
+ let priorityContextBeforeReconciliation : PriorityLevel = NoWork;
111
+
112
// Keeps track of whether we're currently in a work loop. Used to batch
113
// nested updates.
114
let isPerformingWork : boolean = false;
@@ -145,15 +159,6 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
159
}
160
}
161
148
- function getPriorityContext() : PriorityLevel {
149
- let priorityLevel = priorityContext;
150
- // If we're in a batch, downgrade sync priority to task priority
151
- if (priorityLevel === SynchronousPriority && isPerformingWork) {
152
- priorityLevel = TaskPriority;
153
- }
154
- return priorityLevel;
155
- }
156
-
162
function findNextUnitOfWork() {
163
// Clear out roots with no more work on them, or if they have uncaught errors
164
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
486
}
487
488
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
489
+
490
// The current, flushed, state of this fiber is the alternate.
491
// Ideally nothing should rely on this, but relying on it here
492
// 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
497
ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress);
498
}
499
// See if beginning this work spawns more work.
500
+
501
+ priorityContextBeforeReconciliation = priorityContext;
502
+ priorityContext = nextPriorityLevel;
503
let next = beginWork(current, workInProgress, nextPriorityLevel);
504
+ priorityContext = priorityContextBeforeReconciliation;
505
506
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
507
ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress);
@@ -513,10 +523,13 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
523
ReactDebugCurrentFiber.current = null;
524
}
525
526
+
527
+
528
return next;
529
}
530
531
function performFailedUnitOfWork(workInProgress : Fiber) : ?Fiber {
532
+
533
// The current, flushed, state of this fiber is the alternate.
534
// Ideally nothing should rely on this, but relying on it here
535
// 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
540
ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress);
541
}
542
// See if beginning this work spawns more work.
543
+ priorityContextBeforeReconciliation = priorityContext;
544
+ priorityContext = nextPriorityLevel;
545
let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
546
+ priorityContext = priorityContextBeforeReconciliation;
547
548
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
549
ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress);
@@ -686,6 +702,9 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
702
// We caught an error during either the begin or complete phases.
703
const failedWork = nextUnitOfWork;
704
705
+ // Reset the priority context to its value before reconcilation.
706
+ priorityContext = priorityContextBeforeReconciliation;
707
+
708
// "Capture" the error by finding the nearest boundary. If there is no
709
// error boundary, the nearest host container acts as one. If
710
// 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
1021
}
1022
1023
function scheduleUpdateAtPriority(fiber : Fiber, priorityLevel : PriorityLevel) {
1024
+ // If we're in a batch, downgrade sync priority to task priority
1025
+ if (priorityLevel === SynchronousPriority && isPerformingWork) {
1026
+ priorityLevel = TaskPriority;
1027
+ }
1028
+
1029
let node = fiber;
1030
let shouldContinue = true;
1031
while (node && shouldContinue) {
@@ -1040,6 +1064,26 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
1064
scheduleUpdateAtPriority(fiber, TaskPriority);
1065
}
1066
1067
+ function scheduleSetState(fiber : Fiber, partialState : any) {
1068
+ addUpdate(fiber, partialState, priorityContext);
1069
+ scheduleUpdateAtPriority(fiber, priorityContext);
1070
+ }
1071
+
1072
+ function scheduleReplaceState(fiber : Fiber, state : any) {
1073
+ addReplaceUpdate(fiber, state, priorityContext);
1074
+ scheduleUpdateAtPriority(fiber, priorityContext);
1075
+ }
1076
+
1077
+ function scheduleForceUpdate(fiber : Fiber) {
1078
+ addForceUpdate(fiber, priorityContext);
1079
+ scheduleUpdateAtPriority(fiber, priorityContext);
1080
+ }
1081
+
1082
+ function scheduleUpdateCallback(fiber : Fiber, callback : Function) {
1083
+ addCallback(fiber, callback, priorityContext);
1084
+ scheduleUpdateAtPriority(fiber, priorityContext);
1085
+ }
1086
+
1087
function performWithPriority(priorityLevel : PriorityLevel, fn : Function) {
1088
const previousPriorityContext = priorityContext;
1089
priorityContext = priorityLevel;
@@ -1088,7 +1132,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
1132
1133
return {
1134
scheduleWork: scheduleWork,
1091
- getPriorityContext: getPriorityContext,
1135
+ scheduleUpdateCallback: scheduleUpdateCallback,
1136
performWithPriority: performWithPriority,
1137
batchedUpdates: batchedUpdates,
1138
syncUpdates: syncUpdates,
src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
+42
-33
@@ -246,21 +246,15 @@ describe('ReactIncrementalUpdates', () => {
246
]);
247
});
248
249
- it('enqueues setState inside an updater function as if the in-progress update is progressed (and warns)', () => {
250
- spyOn(console, 'error');
249
+ it('gives setState during reconciliation the same priority as whatever level is currently reconciling', () => {
250
let instance;
251
let ops = [];
252
+
253
class Foo extends React.Component {
254
state = {};
255
- componentDidMount() {
256
- ops.push('componentDidMount');
257
- this.setState(function a() {
258
- // Force update b to have Task priority
259
- ReactNoop.syncUpdates(() => {
260
- this.setState({ b: 'b' });
261
- });
262
- return { a: 'a' };
263
- });
255
+ componentWillReceiveProps() {
256
+ ops.push('componentWillReceiveProps');
257
+ this.setState({ b: 'b' });
258
}
259
render() {
260
ops.push('render');
@@ -268,44 +262,59 @@ describe('ReactIncrementalUpdates', () => {
262
return <span prop={Object.keys(this.state).join('')} />;
263
}
264
}
271
-
265
ReactNoop.render(<Foo />);
266
ReactNoop.flush();
274
- expectDev(console.error.calls.count()).toBe(1);
267
+
268
+ ops = [];
269
+
270
+ ReactNoop.performAnimationWork(() => {
271
+ instance.setState({ a: 'a' });
272
+ ReactNoop.render(<Foo />); // Trigger componentWillReceiveProps
273
+ });
274
+ ReactNoop.flush();
275
+
276
expect(ReactNoop.getChildren()).toEqual([span('ab')]);
277
expect(ops).toEqual([
277
- // Initial render
278
- 'render',
279
- 'componentDidMount',
280
- // Updates a and b both have Task priority. Update b is enqueued while
281
- // update a is being processed, but it should be inserted into the queue
282
- // as if update a is already processed. Then processing continues. Because
283
- // they have the same priority, update b is processed in the same batch.
284
- // So there should only be a single render below.
278
+ 'componentWillReceiveProps',
279
'render',
280
]);
281
+ });
282
288
- ops = [];
283
290
- ReactNoop.performAnimationWork(() => {
291
- instance.setState(function c() {
292
- // Update d happens during the begin phase, so it has low priority.
293
- this.setState({ d: 'd' });
294
- return { c: 'c' };
295
- });
284
+ it('enqueues setState inside an updater function as if the in-progress update is progressed (and warns)', () => {
285
+ spyOn(console, 'error');
286
+ let instance;
287
+ let ops = [];
288
+ class Foo extends React.Component {
289
+ state = {};
290
+ render() {
291
+ ops.push('render');
292
+ instance = this;
293
+ return <span prop={Object.keys(this.state).join('')} />;
294
+ }
295
+ }
296
+
297
+ ReactNoop.render(<Foo />);
298
+ ReactNoop.flush();
299
+
300
+ instance.setState(function a() {
301
+ ops.push('setState updater');
302
+ this.setState({ b: 'b' });
303
+ return { a: 'a' };
304
});
305
306
ReactNoop.flush();
299
- expect(ReactNoop.getChildren()).toEqual([span('abcd')]);
307
+ expect(ReactNoop.getChildren()).toEqual([span('ab')]);
308
expect(ops).toEqual([
301
- // Update c has animation priority. Update d is enqueued while c is being
302
- // processed with animation priority. Because d is low priority, it is not
303
- // processed until the next render. So there should be two renders below.
309
+ // Initial render
310
'render',
311
+ 'setState updater',
312
+ // Update b is enqueued with the same priority as update a, so it should
313
+ // be flushed in the same commit.
314
'render',
315
]);
316
308
- expectDev(console.error.calls.count()).toBe(2);
317
+ expectDev(console.error.calls.count()).toBe(1);
318
console.error.calls.reset();
319
});
320
});