@samitouri / QOS-React / commits / 94d2317b4b

setState for Fiber

Updates are scheduled by setting a work priority on the fiber and bubbling it to the root. Because the instance does not know which tree is current at any given time, the update is scheduled on both fiber alternates. Need to add more unit tests to cover edge cases.

Andrew Clark committed Jul 23, 2016 at 18:59 UTC 94d2317b4b9c5c8edb078bb2692e26969a19f0ae
5 files changed +121 -5
src/renderers/shared/fiber/ReactFiber.js
+8
@@ -76,6 +76,10 @@ export type Fiber = Instance & {
76 pendingProps: any, // This type will be more specific once we overload the tag.
77 // TODO: I think that there is a way to merge pendingProps and memoizedProps.
78 memoizedProps: any, // The props used to create the output.
79 + // Local state for class components. May need better naming to disambiguate
80 + // from stateNode.
81 + pendingState: any,
82 + memoizedState: any, // The state used to create the output.
83 // Output is the return value of this fiber, or a linked list of return values
84 // if this returns multiple values. Such as a fragment.
85 output: any, // This type will be more specific once we overload the tag.
@@ -151,6 +155,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
155
156 pendingProps: null,
157 memoizedProps: null,
158 + pendingState: null,
159 + memoizedState: null,
160 output: null,
161
162 nextEffect: null,
@@ -192,6 +198,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
198 alt.sibling = fiber.sibling; // This should always be overridden. TODO: null
199 alt.ref = fiber.ref;
200 alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument.
201 + alt.pendingState = fiber.pendingState;
202 alt.pendingWorkPriority = priorityLevel;
203
204 alt.child = fiber.child;
@@ -217,6 +224,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
224 // pendingProps is here for symmetry but is unnecessary in practice for now.
225 // TODO: Pass in the new pendingProps as an argument maybe?
226 alt.pendingProps = fiber.pendingProps;
227 + alt.pendingState = fiber.pendingState;
228 alt.pendingWorkPriority = priorityLevel;
229
230 alt.memoizedProps = fiber.memoizedProps;
src/renderers/shared/fiber/ReactFiberBeginWork.js
+63 -3
@@ -14,7 +14,9 @@
14
15 import type { ReactCoroutine } from 'ReactCoroutine';
16 import type { Fiber } from 'ReactFiber';
17 +import type { FiberRoot } from 'ReactFiberRoot';
18 import type { HostConfig } from 'ReactFiberReconciler';
19 +import type { Scheduler } from 'ReactFiberScheduler';
20 import type { PriorityLevel } from 'ReactPriorityLevel';
21
22 var {
@@ -22,6 +24,7 @@ var {
24 reconcileChildFibersInPlace,
25 cloneChildFibers,
26 } = require('ReactChildFiber');
27 +var { LowPriority } = require('ReactPriorityLevel');
28 var ReactTypeOfWork = require('ReactTypeOfWork');
29 var {
30 IndeterminateComponent,
@@ -38,7 +41,7 @@ var {
41 OffscreenPriority,
42 } = require('ReactPriorityLevel');
43
41 -module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
44 +module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getScheduler: () => Scheduler) {
45
46 function markChildAsProgressed(current, workInProgress, priorityLevel) {
47 // We now have clones. Let's store them as the currently progressed work.
@@ -105,25 +108,81 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
108 return workInProgress.child;
109 }
110
111 + function updateFiber(fiber: Fiber, state: any, priorityLevel : PriorityLevel): void {
112 + const { scheduleLowPriWork } = getScheduler();
113 + fiber.pendingState = state;
114 +
115 + while (true) {
116 + if (fiber.pendingWorkPriority === NoWork ||
117 + fiber.pendingWorkPriority >= priorityLevel) {
118 + fiber.pendingWorkPriority = priorityLevel;
119 + }
120 + // Duck type root
121 + if (fiber.stateNode && fiber.stateNode.containerInfo) {
122 + const root : FiberRoot = (fiber.stateNode : any);
123 + scheduleLowPriWork(root, priorityLevel);
124 + return;
125 + }
126 + if (!fiber.return) {
127 + throw new Error('No root!');
128 + }
129 + fiber = fiber.return;
130 + }
131 + }
132 +
133 + // Class component state updater
134 + const updater = {
135 + enqueueSetState(instance, partialState) {
136 + const fiber = instance._fiber;
137 +
138 + const prevState = fiber.pendingState || fiber.memoizedState;
139 + const state = Object.assign({}, prevState, partialState);
140 +
141 + // Must schedule an update on both alternates, because we don't know tree
142 + // is current.
143 + updateFiber(fiber, state, LowPriority);
144 + if (fiber.alternate) {
145 + updateFiber(fiber.alternate, state, LowPriority);
146 + }
147 + },
148 + };
149 +
150 function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
151 + // A class component update is the result of either new props or new state.
152 + // Account for the possibly of missing pending props or state by falling
153 + // back to the most recent props or state.
154 var props = workInProgress.pendingProps;
155 + var state = workInProgress.pendingState;
156 + if (!props && current) {
157 + props = current.memoizedProps;
158 + }
159 + if (!state && current) {
160 + state = current.memoizedState;
161 + }
162 +
163 var instance = workInProgress.stateNode;
164 if (!instance) {
165 var ctor = workInProgress.type;
166 workInProgress.stateNode = instance = new ctor(props);
167 + state = workInProgress.pendingState = instance.state || null;
168 + // The instance needs access to the fiber so that it can schedule updates
169 + instance._fiber = workInProgress;
170 + instance.updater = updater;
171 } else if (typeof instance.shouldComponentUpdate === 'function') {
172 if (workInProgress.memoizedProps !== null) {
173 // Reset the props, in case this is a ping-pong case rather than a
174 // completed update case. For the completed update case, the instance
175 // props will already be the memoizedProps.
176 instance.props = workInProgress.memoizedProps;
120 - if (!instance.shouldComponentUpdate(props)) {
177 + instance.state = workInProgress.memoizedState;
178 + if (!instance.shouldComponentUpdate(props, state)) {
179 return bailoutOnAlreadyFinishedWork(current, workInProgress);
180 }
181 }
182 }
183
184 instance.props = props;
185 + instance.state = state;
186 var nextChildren = instance.render();
187 reconcileChildren(current, workInProgress, nextChildren);
188
@@ -253,7 +312,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
312
313 if (workInProgress.pendingProps === null || (
314 workInProgress.memoizedProps !== null &&
256 - workInProgress.pendingProps === workInProgress.memoizedProps
315 + workInProgress.pendingProps === workInProgress.memoizedProps &&
316 + workInProgress.pendingState === workInProgress.memoizedState
317 )) {
318 return bailoutOnAlreadyFinishedWork(current, workInProgress);
319 }
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+1
@@ -67,6 +67,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
67 // the linked list of fibers that has the individual output values.
68 returnFiber.output = (child && !child.sibling) ? child.output : child;
69 returnFiber.memoizedProps = returnFiber.pendingProps;
70 + returnFiber.memoizedState = returnFiber.pendingState;
71 }
72
73 function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
src/renderers/shared/fiber/ReactFiberScheduler.js
+17 -2
@@ -29,9 +29,19 @@ var {
29
30 var timeHeuristicForUnitOfWork = 1;
31
32 +export type Scheduler = {
33 + scheduleLowPriWork: (root : FiberRoot, priority : PriorityLevel) => void
34 +};
35 +
36 module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
37 + // Use a closure to circumvent the circular dependency between the scheduler
38 + // and ReactFiberBeginWork. Don't know if there's a better way to do this.
39 + let scheduler;
40 + function getScheduler(): Scheduler {
41 + return scheduler;
42 + }
43
34 - const { beginWork } = ReactFiberBeginWork(config);
44 + const { beginWork } = ReactFiberBeginWork(config, getScheduler);
45 const { completeWork } = ReactFiberCompleteWork(config);
46 const { commitWork } = ReactFiberCommitWork(config);
47
@@ -133,6 +143,10 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
143 // The work is now done. We don't need this anymore. This flags
144 // to the system not to redo any work here.
145 workInProgress.pendingProps = null;
146 + workInProgress.pendingState = null;
147 + if (current) {
148 + current.pendingState = null;
149 + }
150
151 const returnFiber = workInProgress.return;
152
@@ -259,7 +273,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
273 }
274 */
275
262 - return {
276 + scheduler = {
277 scheduleLowPriWork: scheduleLowPriWork,
278 };
279 + return scheduler;
280 };
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+32
@@ -556,4 +556,36 @@ describe('ReactIncremental', () => {
556 expect(ops).toEqual(['Content', 'Bar', 'Middle']);
557
558 });
559 +
560 + it('can update in the middle of a tree using setState', () => {
561 + let instance;
562 + let states = [];
563 +
564 + class Bar extends React.Component {
565 + constructor() {
566 + super();
567 + this.state = { string: 'a' };
568 + instance = this;
569 + }
570 + render() {
571 + states.push(this.state.string);
572 + return <div>{this.props.children}</div>;
573 + }
574 + }
575 +
576 + function Foo() {
577 + return (
578 + <div>
579 + <Bar />
580 + </div>
581 + );
582 + }
583 +
584 + ReactNoop.render(<Foo />);
585 + ReactNoop.flush();
586 + expect(states).toEqual(['a']);
587 + instance.setState({ string: 'b' });
588 + ReactNoop.flush();
589 + expect(states).toEqual(['a', 'b']);
590 + });
591 });