Skip to content

Commit c948425

Browse files
committed
Separate priority field for pending updates
Currently there is a single priority field `pendingWorkPriority` that represents the priority of the entire subtree. The scheduler does a breadth-first search to find the next unit of work with a matching priority level. If a subtree's priority does not match, that entire subtree is skipped. That means when an update is scheduled deep in the tree (`setState`), its priority must be bubbled to the top of the tree. The problem is that this causes the all the parent priorities to be overridden — there's no way to schedule a high priority update inside a low priority tree without increasing the priority of the entire tree. To address this, this PR introduces a separate priority field called `pendingUpdatePriority` (not 100% sold on the name). The new field represents the priority of the pending props and state, which may be lower than the priority of the entire tree. Now, when the scheduler is searching for work to perform, it only matches if the ` pendingUpdatePriority` matches. It continues to use the `pendingWorkPriority` as a way to ignore subtrees if they do not match. (To elaborate further: because the work priority is the highest priority of any node in the entire tree, if it does not match, the tree can be skipped.)
1 parent 995a02d commit c948425

File tree

7 files changed

+122
-13
lines changed

7 files changed

+122
-13
lines changed

src/renderers/shared/fiber/ReactChildFiber.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var {
2626

2727
var ReactFiber = require('ReactFiber');
2828
var ReactReifiedYield = require('ReactReifiedYield');
29+
var ReactPriorityLevel = require('ReactPriorityLevel');
2930

3031
const {
3132
cloneFiber,
@@ -38,6 +39,10 @@ const {
3839
createReifiedYield,
3940
} = ReactReifiedYield;
4041

42+
const {
43+
NoWork,
44+
} = ReactPriorityLevel;
45+
4146
const isArray = Array.isArray;
4247

4348
function ChildReconciler(shouldClone) {
@@ -64,7 +69,11 @@ function ChildReconciler(shouldClone) {
6469
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
6570
if (!shouldClone) {
6671
// TODO: This might be lowering the priority of nested unfinished work.
67-
clone.pendingWorkPriority = priority;
72+
clone.pendingUpdatePriority = priority;
73+
if (clone.pendingWorkPriority === NoWork ||
74+
clone.pendingWorkPriority > priority) {
75+
clone.pendingWorkPriority = priority;
76+
}
6877
}
6978
clone.pendingProps = element.props;
7079
// clone.child = existingChild.child;
@@ -136,7 +145,11 @@ function ChildReconciler(shouldClone) {
136145
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
137146
if (!shouldClone) {
138147
// TODO: This might be lowering the priority of nested unfinished work.
139-
clone.pendingWorkPriority = priority;
148+
clone.pendingUpdatePriority = priority;
149+
if (clone.pendingWorkPriority === NoWork ||
150+
clone.pendingWorkPriority > priority) {
151+
clone.pendingWorkPriority = priority;
152+
}
140153
}
141154
clone.pendingProps = element.props;
142155
// clone.child = existingChild.child;

src/renderers/shared/fiber/ReactFiber.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ export type Fiber = Instance & {
9696
firstEffect: ?Fiber,
9797
lastEffect: ?Fiber,
9898

99-
// This will be used to quickly determine if a subtree has no pending changes.
99+
// The update priority is the priority of a fiber's pending props and state.
100+
// It may be lower than the priority of the entire subtree.
101+
pendingUpdatePriority: PriorityLevel,
102+
103+
// The work priority is the priority of the entire subtree. It will be used to
104+
// quickly determine if a subtree has no pending changes.
100105
pendingWorkPriority: PriorityLevel,
101106

102107
// This value represents the priority level that was last used to process this
@@ -167,6 +172,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
167172
firstEffect: null,
168173
lastEffect: null,
169174

175+
pendingUpdatePriority: NoWork,
170176
pendingWorkPriority: NoWork,
171177
progressedPriority: NoWork,
172178
progressedChild: null,
@@ -191,6 +197,28 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
191197
// fiber.progressedPriority = NoWork;
192198
// fiber.progressedChild = null;
193199

200+
// Don't deprioritize when cloning. Unlike other priority comparisons (e.g.
201+
// in the scheduler), this one must check that priorityLevel is not equal to
202+
// NoWork, otherwise work will be dropped. For complete correctness, the other
203+
// priority comparisons should also perform this check, even though it's not
204+
// an issue in practice. I didn't catch this at first and it created a subtle
205+
// bug, which suggests we may need to extract the logic into a
206+
// utility function (shouldOverridePriority).
207+
let updatePriority;
208+
let workPriority;
209+
if (priorityLevel !== NoWork &&
210+
(priorityLevel < fiber.pendingUpdatePriority || fiber.pendingUpdatePriority === NoWork)) {
211+
updatePriority = priorityLevel;
212+
} else {
213+
updatePriority = fiber.pendingUpdatePriority;
214+
}
215+
if (updatePriority !== NoWork &&
216+
(updatePriority < fiber.pendingWorkPriority || fiber.pendingWorkPriority === NoWork)) {
217+
workPriority = updatePriority;
218+
} else {
219+
workPriority = fiber.pendingWorkPriority;
220+
}
221+
194222
// We use a double buffering pooling technique because we know that we'll only
195223
// ever need at most two versions of a tree. We pool the "other" unused node
196224
// that we're free to reuse. This is lazily created to avoid allocating extra
@@ -204,7 +232,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
204232
alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument.
205233
alt.updateQueue = fiber.updateQueue;
206234
alt.callbackList = fiber.callbackList;
207-
alt.pendingWorkPriority = priorityLevel;
235+
alt.pendingUpdatePriority = updatePriority;
236+
alt.pendingWorkPriority = workPriority;
208237

209238
alt.child = fiber.child;
210239
alt.memoizedProps = fiber.memoizedProps;
@@ -231,7 +260,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
231260
alt.pendingProps = fiber.pendingProps;
232261
alt.updateQueue = fiber.updateQueue;
233262
alt.callbackList = fiber.callbackList;
234-
alt.pendingWorkPriority = priorityLevel;
263+
alt.pendingUpdatePriority = updatePriority;
264+
alt.pendingWorkPriority = workPriority;
235265

236266
alt.memoizedProps = fiber.memoizedProps;
237267
alt.output = fiber.output;
@@ -253,6 +283,7 @@ exports.createFiberFromElement = function(element : ReactElement<*>, priorityLev
253283
// $FlowFixMe: ReactElement.key is currently defined as ?string but should be defined as null | string in Flow.
254284
const fiber = createFiberFromElementType(element.type, element.key);
255285
fiber.pendingProps = element.props;
286+
fiber.pendingUpdatePriority = priorityLevel;
256287
fiber.pendingWorkPriority = priorityLevel;
257288
return fiber;
258289
};
@@ -282,6 +313,7 @@ exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine, priority
282313
const fiber = createFiber(CoroutineComponent, coroutine.key);
283314
fiber.type = coroutine.handler;
284315
fiber.pendingProps = coroutine;
316+
fiber.pendingUpdatePriority = priorityLevel;
285317
fiber.pendingWorkPriority = priorityLevel;
286318
return fiber;
287319
};

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
6464
}
6565

6666
function reconcileChildren(current, workInProgress, nextChildren) {
67-
const priorityLevel = workInProgress.pendingWorkPriority;
67+
const priorityLevel = workInProgress.pendingUpdatePriority;
6868
reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel);
6969
}
7070

@@ -124,14 +124,29 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
124124
if (fiber.alternate) {
125125
fiber.alternate.updateQueue = updateQueue;
126126
}
127+
128+
// Set the update priority of the fiber and its alternate
129+
if (fiber.pendingUpdatePriority === NoWork ||
130+
fiber.pendingUpdatePriority > priorityLevel) {
131+
fiber.pendingUpdatePriority = priorityLevel;
132+
}
133+
if (fiber.alternate) {
134+
if (fiber.alternate.pendingUpdatePriority === NoWork ||
135+
fiber.alternate.pendingUpdatePriority > priorityLevel) {
136+
fiber.alternate.pendingUpdatePriority = priorityLevel;
137+
}
138+
}
139+
140+
// For this fiber and all its ancestors and their alternates, set the
141+
// work (subtree) priority
127142
while (true) {
128143
if (fiber.pendingWorkPriority === NoWork ||
129-
fiber.pendingWorkPriority >= priorityLevel) {
144+
fiber.pendingWorkPriority > priorityLevel) {
130145
fiber.pendingWorkPriority = priorityLevel;
131146
}
132147
if (fiber.alternate) {
133148
if (fiber.alternate.pendingWorkPriority === NoWork ||
134-
fiber.alternate.pendingWorkPriority >= priorityLevel) {
149+
fiber.alternate.pendingWorkPriority > priorityLevel) {
135150
fiber.alternate.pendingWorkPriority = priorityLevel;
136151
}
137152
}
@@ -235,7 +250,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
235250
function updateHostComponent(current, workInProgress) {
236251
const nextChildren = workInProgress.pendingProps.children;
237252
if (workInProgress.pendingProps.hidden &&
238-
workInProgress.pendingWorkPriority !== OffscreenPriority) {
253+
workInProgress.pendingUpdatePriority !== OffscreenPriority) {
239254
// If this host component is hidden, we can bail out on the children.
240255
// We'll rerender the children later at the lower priority.
241256

@@ -311,7 +326,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
311326
*/
312327

313328
function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber {
314-
const priorityLevel = workInProgress.pendingWorkPriority;
329+
const priorityLevel = workInProgress.pendingUpdatePriority;
315330

316331
// TODO: We should ideally be able to bail out early if the children have no
317332
// more work to do. However, since we don't have a separation of this

src/renderers/shared/fiber/ReactFiberCompleteWork.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
113113

114114
var currentFirstChild = current ? current.stateNode : null;
115115
// Inherit the priority of the returnFiber.
116-
const priority = workInProgress.pendingWorkPriority;
116+
const priority = workInProgress.pendingUpdatePriority;
117117
workInProgress.stateNode = reconcileChildFibers(
118118
workInProgress,
119119
currentFirstChild,

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconci
7373
// TODO: This should not override the pendingWorkPriority if there is
7474
// higher priority work in the subtree.
7575
container.pendingProps = element;
76+
container.pendingUpdatePriority = LowPriority;
7677
container.pendingWorkPriority = LowPriority;
7778

7879
scheduleLowPriWork(root, LowPriority);
@@ -88,6 +89,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconci
8889
const root : FiberRoot = (container.stateNode : any);
8990
// TODO: Use pending work/state instead of props.
9091
root.current.pendingProps = element;
92+
root.current.pendingUpdatePriority = LowPriority;
9193
root.current.pendingWorkPriority = LowPriority;
9294

9395
scheduleLowPriWork(root, LowPriority);
@@ -98,6 +100,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconci
98100
const root : FiberRoot = (container.stateNode : any);
99101
// TODO: Use pending work/state instead of props.
100102
root.current.pendingProps = [];
103+
root.current.pendingUpdatePriority = LowPriority;
101104
root.current.pendingWorkPriority = LowPriority;
102105

103106
scheduleLowPriWork(root, LowPriority);

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
7575
let highestPriorityLevel = NoWork;
7676
while (root) {
7777
if (highestPriorityLevel === NoWork ||
78-
highestPriorityLevel > root.current.pendingWorkPriority) {
79-
highestPriorityLevel = root.current.pendingWorkPriority;
78+
highestPriorityLevel > root.current.pendingUpdatePriority) {
79+
highestPriorityLevel = root.current.pendingUpdatePriority;
8080
highestPriorityRoot = root;
8181
}
8282
// We didn't find anything to do in this root, so let's try the next one.
@@ -137,6 +137,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
137137
const current = workInProgress.alternate;
138138
const next = completeWork(current, workInProgress);
139139

140+
workInProgress.pendingUpdatePriority = NoWork;
140141
resetWorkPriority(workInProgress);
141142

142143
// The work is now done. We don't need this anymore. This flags

src/renderers/shared/fiber/__tests__/ReactIncremental-test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,51 @@ describe('ReactIncremental', function() {
692692
expect(instance.state.called).toEqual(true);
693693
});
694694

695+
it('can setState without overriding its parents\' priority', () => {
696+
let ops = [];
697+
let instance;
698+
class Baz extends React.Component {
699+
constructor() {
700+
super();
701+
instance = this;
702+
this.state = { num: 0 };
703+
}
704+
render() {
705+
ops.push('Baz');
706+
return <span />;
707+
}
708+
}
709+
710+
function Bar({ id }) {
711+
ops.push('Bar' + id);
712+
return <div />;
713+
}
714+
715+
function Foo() {
716+
ops.push('Foo');
717+
return [
718+
<section hidden={true}>
719+
<Bar id={1} />
720+
<Baz />
721+
</section>,
722+
<Bar id={2} />,
723+
];
724+
}
725+
726+
ReactNoop.render(<Foo />);
727+
ReactNoop.flush();
728+
expect(ops).toEqual(['Foo', 'Bar2', 'Bar1', 'Baz']);
729+
ops = [];
730+
ReactNoop.render(<Foo />);
731+
// Even though Baz is in a hidden subtree, calling setState gives it a
732+
// higher priority. It should not affect the priority of anything else in
733+
// the subtree.
734+
instance.setState({ num: 1 });
735+
ReactNoop.flush();
736+
// Baz should come before Bar1 because it has higher priority
737+
expect(ops).toEqual(['Foo', 'Bar2', 'Baz', 'Bar1']);
738+
});
739+
695740
it('can replaceState', () => {
696741
let instance;
697742
const Bar = React.createClass({

0 commit comments

Comments
 (0)