@samitouri / QOS-React / commits / af7dd8e0a1

Move wip fibers to childInProgress

This took a while to figure out, but we need to be able to store children that are currently being worked on separately from the current children. We always need a canonical "current" children so that we can update them. However, we also need a different set that we're currently working on so that we have a way to get to already progressed work. This solve the starvation problem in the first render because now we can reach children that were never rendered and have a place to store their progressed work on. The unit test changes tests this. This lets us get rid of the hasWorkInProgress flag. When we reconcile new children we need to reconcile them against progressed work so that we can reuse it. The progressed work is "work in progress" nodes. So in that case we need to mutate instead of clone, to preserve the invariant that only two versions exist at any given point. This effectively forks the ReactChildFiber implementation.

Sebastian Markbage committed Aug 5, 2016 at 14:33 UTC af7dd8e0a13600870a18b244608cfa37da5ae21c
9 files changed +306 -228
src/renderers/noop/ReactNoop.js
+13 -1
@@ -167,9 +167,21 @@ var ReactNoop = {
167
168 function logFiber(fiber : Fiber, depth) {
169 console.log(
170 - ' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'),
170 + ' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'),
171 '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']'
172 );
173 + const childInProgress = fiber.childInProgress;
174 + if (childInProgress) {
175 + if (childInProgress === fiber.child) {
176 + console.log(' '.repeat(depth + 1) + 'ERROR: IN PROGRESS == CURRENT');
177 + } else {
178 + console.log(' '.repeat(depth + 1) + 'IN PROGRESS');
179 + logFiber(childInProgress, depth + 1);
180 + if (fiber.child) {
181 + console.log(' '.repeat(depth + 1) + 'CURRENT');
182 + }
183 + }
184 + }
185 if (fiber.child) {
186 logFiber(fiber.child, depth + 1);
187 }
src/renderers/shared/fiber/ReactChildFiber.js
+144 -127
@@ -42,151 +42,168 @@ const {
42
43 const isArray = Array.isArray;
44
45 -function createSubsequentChild(
46 - returnFiber : Fiber,
47 - existingChild : ?Fiber,
48 - previousSibling : Fiber,
49 - newChildren,
50 - priority : PriorityLevel
51 -) : Fiber {
52 - if (typeof newChildren !== 'object' || newChildren === null) {
53 - return previousSibling;
54 - }
45 +function ChildReconciler(shouldClone) {
46 +
47 + function createSubsequentChild(
48 + returnFiber : Fiber,
49 + existingChild : ?Fiber,
50 + previousSibling : Fiber,
51 + newChildren,
52 + priority : PriorityLevel
53 + ) : Fiber {
54 + if (typeof newChildren !== 'object' || newChildren === null) {
55 + return previousSibling;
56 + }
57
56 - switch (newChildren.$$typeof) {
57 - case REACT_ELEMENT_TYPE: {
58 - const element = (newChildren : ReactElement<any>);
59 - if (existingChild &&
60 - element.type === existingChild.type &&
61 - element.key === existingChild.key) {
62 - // TODO: This is not sufficient since previous siblings could be new.
63 - // Will fix reconciliation properly later.
64 - const clone = cloneFiber(existingChild, priority);
65 - clone.pendingProps = element.props;
66 - clone.child = existingChild.child;
67 - clone.sibling = null;
68 - clone.return = returnFiber;
69 - previousSibling.sibling = clone;
70 - return clone;
58 + switch (newChildren.$$typeof) {
59 + case REACT_ELEMENT_TYPE: {
60 + const element = (newChildren : ReactElement<any>);
61 + if (existingChild &&
62 + element.type === existingChild.type &&
63 + element.key === existingChild.key) {
64 + // TODO: This is not sufficient since previous siblings could be new.
65 + // Will fix reconciliation properly later.
66 + const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
67 + if (!shouldClone) {
68 + clone.pendingWorkPriority = priority;
69 + }
70 + clone.pendingProps = element.props;
71 + clone.child = existingChild.child;
72 + clone.sibling = null;
73 + clone.return = returnFiber;
74 + previousSibling.sibling = clone;
75 + return clone;
76 + }
77 + const child = createFiberFromElement(element, priority);
78 + previousSibling.sibling = child;
79 + child.return = returnFiber;
80 + return child;
81 }
72 - const child = createFiberFromElement(element, priority);
73 - previousSibling.sibling = child;
74 - child.return = returnFiber;
75 - return child;
76 - }
82
78 - case REACT_COROUTINE_TYPE: {
79 - const coroutine = (newChildren : ReactCoroutine);
80 - const child = createFiberFromCoroutine(coroutine, priority);
81 - previousSibling.sibling = child;
82 - child.return = returnFiber;
83 - return child;
84 - }
83 + case REACT_COROUTINE_TYPE: {
84 + const coroutine = (newChildren : ReactCoroutine);
85 + const child = createFiberFromCoroutine(coroutine, priority);
86 + previousSibling.sibling = child;
87 + child.return = returnFiber;
88 + return child;
89 + }
90
86 - case REACT_YIELD_TYPE: {
87 - const yieldNode = (newChildren : ReactYield);
88 - const reifiedYield = createReifiedYield(yieldNode);
89 - const child = createFiberFromYield(yieldNode, priority);
90 - child.output = reifiedYield;
91 - previousSibling.sibling = child;
92 - child.return = returnFiber;
93 - return child;
91 + case REACT_YIELD_TYPE: {
92 + const yieldNode = (newChildren : ReactYield);
93 + const reifiedYield = createReifiedYield(yieldNode);
94 + const child = createFiberFromYield(yieldNode, priority);
95 + child.output = reifiedYield;
96 + previousSibling.sibling = child;
97 + child.return = returnFiber;
98 + return child;
99 + }
100 }
95 - }
101
97 - if (isArray(newChildren)) {
98 - let prev : Fiber = previousSibling;
99 - let existing : ?Fiber = existingChild;
100 - for (var i = 0; i < newChildren.length; i++) {
101 - prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
102 - if (prev && existing) {
103 - // TODO: This is not correct because there could've been more
104 - // than one sibling consumed but I don't want to return a tuple.
105 - existing = existing.sibling;
102 + if (isArray(newChildren)) {
103 + let prev : Fiber = previousSibling;
104 + let existing : ?Fiber = existingChild;
105 + for (var i = 0; i < newChildren.length; i++) {
106 + var nextExisting = existing && existing.sibling;
107 + prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
108 + if (prev && existing) {
109 + // TODO: This is not correct because there could've been more
110 + // than one sibling consumed but I don't want to return a tuple.
111 + existing = nextExisting;
112 + }
113 }
114 + return prev;
115 + } else {
116 + // TODO: Throw for unknown children.
117 + return previousSibling;
118 }
108 - return prev;
109 - } else {
110 - // TODO: Throw for unknown children.
111 - return previousSibling;
119 }
113 -}
120
115 -function createFirstChild(returnFiber, existingChild, newChildren, priority) {
116 - if (typeof newChildren !== 'object' || newChildren === null) {
117 - return null;
118 - }
121 + function createFirstChild(returnFiber, existingChild, newChildren, priority) {
122 + if (typeof newChildren !== 'object' || newChildren === null) {
123 + return null;
124 + }
125
120 - switch (newChildren.$$typeof) {
121 - case REACT_ELEMENT_TYPE: {
122 - const element = (newChildren : ReactElement<any>);
123 - if (existingChild &&
124 - element.type === existingChild.type &&
125 - element.key === existingChild.key) {
126 - // Get the clone of the existing fiber.
127 - const clone = cloneFiber(existingChild, priority);
128 - clone.pendingProps = element.props;
129 - clone.child = existingChild.child;
130 - clone.sibling = null;
131 - clone.return = returnFiber;
132 - return clone;
126 + switch (newChildren.$$typeof) {
127 + case REACT_ELEMENT_TYPE: {
128 + const element = (newChildren : ReactElement<any>);
129 + if (existingChild &&
130 + element.type === existingChild.type &&
131 + element.key === existingChild.key) {
132 + // Get the clone of the existing fiber.
133 + const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
134 + if (!shouldClone) {
135 + clone.pendingWorkPriority = priority;
136 + }
137 + clone.pendingProps = element.props;
138 + clone.child = existingChild.child;
139 + clone.sibling = null;
140 + clone.return = returnFiber;
141 + return clone;
142 + }
143 + const child = createFiberFromElement(element, priority);
144 + child.return = returnFiber;
145 + return child;
146 }
134 - const child = createFiberFromElement(element, priority);
135 - child.return = returnFiber;
136 - return child;
137 - }
147
139 - case REACT_COROUTINE_TYPE: {
140 - const coroutine = (newChildren : ReactCoroutine);
141 - const child = createFiberFromCoroutine(coroutine, priority);
142 - child.return = returnFiber;
143 - return child;
144 - }
148 + case REACT_COROUTINE_TYPE: {
149 + const coroutine = (newChildren : ReactCoroutine);
150 + const child = createFiberFromCoroutine(coroutine, priority);
151 + child.return = returnFiber;
152 + return child;
153 + }
154
146 - case REACT_YIELD_TYPE: {
147 - // A yield results in a fragment fiber whose output is the continuation.
148 - // TODO: When there is only a single child, we can optimize this to avoid
149 - // the fragment.
150 - const yieldNode = (newChildren : ReactYield);
151 - const reifiedYield = createReifiedYield(yieldNode);
152 - const child = createFiberFromYield(yieldNode, priority);
153 - child.output = reifiedYield;
154 - child.return = returnFiber;
155 - return child;
155 + case REACT_YIELD_TYPE: {
156 + // A yield results in a fragment fiber whose output is the continuation.
157 + // TODO: When there is only a single child, we can optimize this to avoid
158 + // the fragment.
159 + const yieldNode = (newChildren : ReactYield);
160 + const reifiedYield = createReifiedYield(yieldNode);
161 + const child = createFiberFromYield(yieldNode, priority);
162 + child.output = reifiedYield;
163 + child.return = returnFiber;
164 + return child;
165 + }
166 }
157 - }
167
159 - if (isArray(newChildren)) {
160 - var first : ?Fiber = null;
161 - var prev : ?Fiber = null;
162 - var existing : ?Fiber = existingChild;
163 - for (var i = 0; i < newChildren.length; i++) {
164 - if (prev == null) {
165 - prev = createFirstChild(returnFiber, existing, newChildren[i], priority);
166 - first = prev;
167 - } else {
168 - prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
169 - }
170 - if (prev && existing) {
171 - // TODO: This is not correct because there could've been more
172 - // than one sibling consumed but I don't want to return a tuple.
173 - existing = existing.sibling;
168 + if (isArray(newChildren)) {
169 + var first : ?Fiber = null;
170 + var prev : ?Fiber = null;
171 + var existing : ?Fiber = existingChild;
172 + for (var i = 0; i < newChildren.length; i++) {
173 + var nextExisting = existing && existing.sibling;
174 + if (prev == null) {
175 + prev = createFirstChild(returnFiber, existing, newChildren[i], priority);
176 + first = prev;
177 + } else {
178 + prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
179 + }
180 + if (prev && existing) {
181 + // TODO: This is not correct because there could've been more
182 + // than one sibling consumed but I don't want to return a tuple.
183 + existing = nextExisting;
184 + }
185 }
186 + return first;
187 + } else {
188 + // TODO: Throw for unknown children.
189 + return null;
190 }
176 - return first;
177 - } else {
178 - // TODO: Throw for unknown children.
179 - return null;
191 }
192 +
193 + // TODO: This API won't work because we'll need to transfer the side-effects of
194 + // unmounting children to the returnFiber.
195 + function reconcileChildFibers(
196 + returnFiber : Fiber,
197 + currentFirstChild : ?Fiber,
198 + newChildren : ReactNodeList,
199 + priority : PriorityLevel
200 + ) : ?Fiber {
201 + return createFirstChild(returnFiber, currentFirstChild, newChildren, priority);
202 + }
203 +
204 + return reconcileChildFibers;
205 }
206
183 -// TODO: This API won't work because we'll need to transfer the side-effects of
184 -// unmounting children to the returnFiber.
185 -exports.reconcileChildFibers = function(
186 - returnFiber : Fiber,
187 - currentFirstChild : ?Fiber,
188 - newChildren : ReactNodeList,
189 - priority : PriorityLevel
190 -) : ?Fiber {
191 - return createFirstChild(returnFiber, currentFirstChild, newChildren, priority);
192 -};
207 +exports.reconcileChildFibers = ChildReconciler(true);
208 +
209 +exports.reconcileChildFibersInPlace = ChildReconciler(false);
src/renderers/shared/fiber/ReactFiber.js
+7 -14
@@ -100,17 +100,9 @@ export type Fiber = Instance & {
100 // memory if we need to.
101 alternate: ?Fiber,
102
103 - // Keeps track if we've completed this node or if we're currently in the
104 - // middle of processing it. We really should know this based on pendingProps
105 - // or something else. We could also reuse the tag for this purpose. However,
106 - // I'm not really sure so I'll use a flag for now.
107 - // TODO: Find another way to infer this flag.
108 - hasWorkInProgress: bool,
109 - // Keeps track if we've deprioritized the children of this node so we know to
110 - // reuse the existing children.
111 - // TODO: Find another way to infer this flag or separate children updates from
112 - // property updates so that we simply don't try to update children here.
113 - wasDeprioritized: bool,
103 + // Keeps track of the children that are currently being processed but have not
104 + // yet completed.
105 + childInProgress: ?Fiber,
106
107 // Conceptual aliases
108 // workInProgress : Fiber -> alternate The alternate used for reuse happens
@@ -150,8 +142,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
142
143 pendingWorkPriority: NoWork,
144
153 - hasWorkInProgress: false,
154 - wasDeprioritized: false,
145 + childInProgress: null,
146
147 alternate: null,
148
@@ -173,6 +164,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
164 if (alt) {
165 alt.stateNode = fiber.stateNode;
166 alt.child = fiber.child;
167 + alt.childInProgress = fiber.childInProgress;
168 alt.sibling = fiber.sibling;
169 alt.ref = alt.ref;
170 alt.pendingProps = fiber.pendingProps;
@@ -192,6 +184,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
184 alt.type = fiber.type;
185 alt.stateNode = fiber.stateNode;
186 alt.child = fiber.child;
187 + alt.childInProgress = fiber.childInProgress;
188 alt.sibling = fiber.sibling;
189 alt.ref = alt.ref;
190 // pendingProps is here for symmetry but is unnecessary in practice for now.
@@ -232,7 +225,7 @@ function createFiberFromElementType(type : mixed, key : null | string) {
225 throw new Error('Unknown component type: ' + typeof type);
226 }
227 return fiber;
235 -};
228 +}
229
230 exports.createFiberFromElementType = createFiberFromElementType;
231
src/renderers/shared/fiber/ReactFiberBeginWork.js
+68 -52
@@ -16,7 +16,10 @@ import type { ReactCoroutine } from 'ReactCoroutine';
16 import type { Fiber } from 'ReactFiber';
17 import type { HostConfig } from 'ReactFiberReconciler';
18
19 -var { reconcileChildFibers } = require('ReactChildFiber');
19 +var {
20 + reconcileChildFibers,
21 + reconcileChildFibersInPlace,
22 +} = require('ReactChildFiber');
23 var ReactTypeOfWork = require('ReactTypeOfWork');
24 var {
25 IndeterminateComponent,
@@ -38,12 +41,34 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
41
42 function reconcileChildren(current, workInProgress, nextChildren) {
43 const priority = workInProgress.pendingWorkPriority;
41 - workInProgress.child = reconcileChildFibers(
42 - workInProgress,
43 - current ? current.child : null,
44 - nextChildren,
45 - priority
46 - );
44 + reconcileChildrenAtPriority(current, workInProgress, nextChildren, priority);
45 + }
46 +
47 + function reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel) {
48 + if (current && current.childInProgress) {
49 + workInProgress.childInProgress = reconcileChildFibersInPlace(
50 + workInProgress,
51 + current.childInProgress,
52 + nextChildren,
53 + priorityLevel
54 + );
55 + // This is now invalid because we reused nodes.
56 + current.childInProgress = null;
57 + } else if (workInProgress.childInProgress) {
58 + workInProgress.childInProgress = reconcileChildFibersInPlace(
59 + workInProgress,
60 + workInProgress.childInProgress,
61 + nextChildren,
62 + priorityLevel
63 + );
64 + } else {
65 + workInProgress.childInProgress = reconcileChildFibers(
66 + workInProgress,
67 + current ? current.child : null,
68 + nextChildren,
69 + priorityLevel
70 + );
71 + }
72 }
73
74 function updateFunctionalComponent(current, workInProgress) {
@@ -68,13 +93,13 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
93 return bailoutOnCurrent(current, workInProgress);
94 }
95 }
71 - if (!workInProgress.hasWorkInProgress && workInProgress.memoizedProps) {
96 + if (!workInProgress.childInProgress && workInProgress.memoizedProps) {
97 // Reset the props, in case this is a ping-pong case rather than a
98 // completed update case. For the completed update case, the instance
99 // props will already be the memoizedProps.
100 instance.props = workInProgress.memoizedProps;
101 if (!instance.shouldComponentUpdate(props)) {
77 - return bailoutOnAlreadyFinishedWork(workInProgress);
102 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
103 }
104 }
105 }
@@ -82,7 +107,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
107 var nextChildren = instance.render();
108 reconcileChildren(current, workInProgress, nextChildren);
109 workInProgress.pendingWorkPriority = NoWork;
85 - return workInProgress.child;
110 + return workInProgress.childInProgress;
111 }
112
113 function updateHostComponent(current, workInProgress) {
@@ -93,25 +118,19 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
118 // If this host component is hidden, we can reconcile its children at
119 // the lowest priority and bail out from this particular pass. Unless, we're
120 // currently reconciling the lowest priority.
96 - workInProgress.child = reconcileChildFibers(
97 - workInProgress,
98 - current ? current.child : null,
99 - nextChildren,
100 - OffscreenPriority
101 - );
121 + // If we have a child in progress already, we reconcile against that set
122 + // to retain any work within it. We'll recreate any component that was in
123 + // the current set and next set but not in the previous in progress set.
124 + // TODO: This attaches a node that hasn't completed rendering so it
125 + // becomes part of the render tree, even though it never completed. Its
126 + // `output` property is unpredictable because of it.
127 + reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority);
128 workInProgress.pendingWorkPriority = OffscreenPriority;
103 - workInProgress.wasDeprioritized = true;
129 return null;
130 } else {
106 - workInProgress.child = reconcileChildFibers(
107 - workInProgress,
108 - current ? current.child : null,
109 - nextChildren,
110 - priority
111 - );
131 + reconcileChildren(current, workInProgress, nextChildren);
132 workInProgress.pendingWorkPriority = NoWork;
113 - workInProgress.wasDeprioritized = false;
114 - return workInProgress.child;
133 + return workInProgress.childInProgress;
134 }
135 }
136
@@ -193,6 +212,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
212 workInProgress.pendingProps = null;
213 workInProgress.pendingWorkPriority = NoWork;
214 workInProgress.stateNode = current.stateNode;
215 + workInProgress.childInProgress = current.childInProgress;
216 if (current.child) {
217 // If we bail out but still has work with the current priority in this
218 // subtree, we need to go find it right now. If we don't, we won't flush
@@ -213,7 +233,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
233 }
234 }
235
216 - function bailoutOnAlreadyFinishedWork(workInProgress : Fiber) : ?Fiber {
236 + function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber {
237 // If we started this work before, and finished it, or if we're in a
238 // ping-pong update scenario, this version could already be what we're
239 // looking for. In that case, we should be able to just bail out.
@@ -225,10 +245,15 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
245 workInProgress.nextEffect = null;
246 workInProgress.lastEffect = null;
247
228 - if (workInProgress.child && workInProgress.child.alternate) {
229 - // On the way up here, we reset the child node to be the current one.
230 - // Therefore we have to reuse the alternate. This is super weird.
231 - let child = workInProgress.child.alternate;
248 + if (workInProgress.child) {
249 + // On the way up here, we reset the child node to be the current one by
250 + // cloning. However, it is really the original child that represents the
251 + // already completed work. Therefore we have to reuse the alternate.
252 + // But if we don't have a current, this was not cloned. This is super weird.
253 + const child = !current ? workInProgress.child : workInProgress.child.alternate;
254 + if (!child) {
255 + throw new Error('We must have a current child to be able to use this.');
256 + }
257 workInProgress.child = child;
258 // Ensure that the effects of reused work are preserved.
259 reuseChildrenEffects(workInProgress, child);
@@ -256,58 +281,49 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
281 return bailoutOnCurrent(current, workInProgress);
282 }
283
259 - if (!workInProgress.hasWorkInProgress &&
284 + if (!workInProgress.childInProgress &&
285 workInProgress.pendingProps === workInProgress.memoizedProps) {
261 - return bailoutOnAlreadyFinishedWork(workInProgress);
286 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
287 }
288
264 - let nextWork;
289 switch (workInProgress.tag) {
290 case IndeterminateComponent:
291 mountIndeterminateComponent(current, workInProgress);
268 - workInProgress.hasWorkInProgress = true;
269 - return workInProgress.child;
292 + return workInProgress.childInProgress;
293 case FunctionalComponent:
294 updateFunctionalComponent(current, workInProgress);
272 - workInProgress.hasWorkInProgress = true;
273 - return workInProgress.child;
295 + return workInProgress.childInProgress;
296 case ClassComponent:
275 - nextWork = updateClassComponent(current, workInProgress);
276 - workInProgress.hasWorkInProgress = true;
277 - return nextWork;
297 + return updateClassComponent(current, workInProgress);
298 case HostContainer:
299 reconcileChildren(current, workInProgress, workInProgress.pendingProps);
300 // A yield component is just a placeholder, we can just run through the
301 // next one immediately.
282 - workInProgress.hasWorkInProgress = true;
302 workInProgress.pendingWorkPriority = NoWork;
284 - if (workInProgress.child) {
303 + if (workInProgress.childInProgress) {
304 return beginWork(
286 - workInProgress.child.alternate,
287 - workInProgress.child
305 + workInProgress.childInProgress.alternate,
306 + workInProgress.childInProgress
307 );
308 }
309 return null;
310 case HostComponent:
292 - nextWork = updateHostComponent(current, workInProgress);
293 - workInProgress.hasWorkInProgress = true;
294 - return nextWork;
311 + return updateHostComponent(current, workInProgress);
312 case CoroutineHandlerPhase:
313 // This is a restart. Reset the tag to the initial phase.
314 workInProgress.tag = CoroutineComponent;
315 // Intentionally fall through since this is now the same.
316 case CoroutineComponent:
317 updateCoroutineComponent(current, workInProgress);
301 - workInProgress.hasWorkInProgress = true;
318 // This doesn't take arbitrary time so we could synchronously just begin
319 // eagerly do the work of workInProgress.child as an optimization.
304 - if (workInProgress.child) {
320 + if (workInProgress.childInProgress) {
321 return beginWork(
306 - workInProgress.child.alternate,
307 - workInProgress.child
322 + workInProgress.childInProgress.alternate,
323 + workInProgress.childInProgress
324 );
325 }
310 - return workInProgress.child;
326 + return workInProgress.childInProgress;
327 case YieldComponent:
328 // A yield component is just a placeholder, we can just run through the
329 // next one immediately.
src/renderers/shared/fiber/ReactFiberCommitWork.js
+1 -8
@@ -47,14 +47,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
47 throw new Error('This should only be done during updates.');
48 }
49 // Commit the work prepared earlier.
50 - let child;
51 - if (finishedWork.wasDeprioritized) {
52 - // If this was a down priority, we need to preserve the old child in
53 - // the output.
54 - child = finishedWork.alternate ? finishedWork.alternate.child : null;
55 - } else {
56 - child = finishedWork.child;
57 - }
50 + const child = finishedWork.child;
51 const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
52 const newProps = finishedWork.memoizedProps;
53 const current = finishedWork.alternate;
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+1 -8
@@ -143,15 +143,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
143 markForPreEffect(workInProgress);
144 return null;
145 case HostComponent:
146 - let child;
146 let newProps = workInProgress.pendingProps;
148 - if (workInProgress.wasDeprioritized) {
149 - // If this was a down priority, we need to preserve the old child in
150 - // the output.
151 - child = current ? current.child : null;
152 - } else {
153 - child = workInProgress.child;
154 - }
147 + const child = workInProgress.child;
148 const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
149 if (current && workInProgress.stateNode != null) {
150 // If we have an alternate, that means this is an update and we need to
src/renderers/shared/fiber/ReactFiberPendingWork.js
+27 -3
@@ -56,9 +56,26 @@ exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLev
56 // because it is the highest priority for the whole subtree.
57 // TODO: Coroutines can have work in their stateNode which is another
58 // type of child that needs to be searched for work.
59 - if (current.child) {
60 - // Ensure we have a work in progress copy to backtrack through.
59 + if (current.childInProgress) {
60 + let workInProgress = current.childInProgress;
61 + while (workInProgress) {
62 + workInProgress.return = current.alternate;
63 + workInProgress = workInProgress.sibling;
64 + }
65 + workInProgress = current.childInProgress;
66 + while (workInProgress) {
67 + // Don't bother drilling further down this tree if there is no child.
68 + if (workInProgress.pendingWorkPriority !== NoWork &&
69 + workInProgress.pendingWorkPriority <= priorityLevel &&
70 + workInProgress.pendingProps !== null) {
71 + return workInProgress;
72 + }
73 + workInProgress = workInProgress.sibling;
74 + }
75 + } else if (current.child) {
76 let currentChild = current.child;
77 + currentChild.return = current;
78 + // Ensure we have a work in progress copy to backtrack through.
79 let workInProgress = current.alternate;
80 if (!workInProgress) {
81 throw new Error('Should have wip now');
@@ -74,7 +91,7 @@ exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLev
91 currentChild.pendingWorkPriority
92 );
93 cloneSiblings(currentChild, workInProgress.child, workInProgress);
77 - current = current.child;
94 + current = currentChild;
95 continue;
96 }
97 // If we match the priority but has no child and no work to do,
@@ -82,6 +99,12 @@ exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLev
99 current.pendingWorkPriority = NoWork;
100 }
101 if (current === currentRoot) {
102 + if (current.pendingWorkPriority <= priorityLevel) {
103 + // If this subtree had work left to do, we would have returned it by
104 + // now. This could happen if a child with pending work gets cleaned up
105 + // but we don't clear the flag then. It is safe to reset it now.
106 + current.pendingWorkPriority = NoWork;
107 + }
108 return null;
109 }
110 while (!current.sibling) {
@@ -96,6 +119,7 @@ exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLev
119 current.pendingWorkPriority = NoWork;
120 }
121 }
122 + current.sibling.return = current.return;
123 current = current.sibling;
124 }
125 return null;
src/renderers/shared/fiber/ReactFiberScheduler.js
+16 -3
@@ -62,6 +62,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
62 }
63 nextScheduledRoot = nextScheduledRoot.nextScheduledRoot;
64 }
65 + // TODO: This is scanning one root at a time. It should be scanning all
66 + // roots for high priority work before moving on to lower priorities.
67 let root = nextScheduledRoot;
68 while (root) {
69 cloneFiber(root.current, root.current.pendingWorkPriority);
@@ -87,6 +89,11 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
89 // We didn't find anything to do in this root, so let's try the next one.
90 root = root.nextScheduledRoot;
91 }
92 + root = nextScheduledRoot;
93 + while (root) {
94 + root = root.nextScheduledRoot;
95 + }
96 +
97 nextPriorityLevel = NoWork;
98 return null;
99 }
@@ -119,9 +126,6 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
126 // The work is now done. We don't need this anymore. This flags
127 // to the system not to redo any work here.
128 workInProgress.pendingProps = null;
122 - if (workInProgress.pendingWorkPriority === NoWork) {
123 - workInProgress.hasWorkInProgress = false;
124 - }
129
130 const returnFiber = workInProgress.return;
131
@@ -155,6 +159,15 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
159 } else if (returnFiber) {
160 // If there's no more work in this returnFiber. Complete the returnFiber.
161 workInProgress = returnFiber;
162 + // If we're stepping up through the child, that means we can now commit
163 + // this work. We should only do this when we're stepping upwards because
164 + // completing a downprioritized item is not the same as completing its
165 + // children.
166 + if (workInProgress.childInProgress) {
167 + workInProgress.child = workInProgress.childInProgress;
168 + workInProgress.childInProgress = null;
169 + }
170 + continue;
171 } else {
172 // If we're at the root, there's no more work to do. We can flush it.
173 const root : FiberRoot = (workInProgress.stateNode : any);
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+29 -12
@@ -283,7 +283,6 @@ describe('ReactIncremental', function() {
283 });
284
285 it('can resume work in a bailed subtree within one pass', function() {
286 -
286 var ops = [];
287
288 function Bar(props) {
@@ -308,6 +307,8 @@ describe('ReactIncremental', function() {
307 return <span>{props.children}</span>;
308 }
309
310 + // Should content not just bail out on current, not workInProgress?
311 +
312 class Content extends React.Component {
313 shouldComponentUpdate() {
314 return false;
@@ -359,7 +360,6 @@ describe('ReactIncremental', function() {
360 // after them which is not correct.
361 ReactNoop.flush();
362 expect(ops).toEqual(['Bar', 'Middle', 'Bar']);
362 -
363 });
364
365 it('can reuse work done after being preempted', function() {
@@ -384,19 +384,23 @@ describe('ReactIncremental', function() {
384 </div>
385 );
386
387 + var step0 = (
388 + <div>
389 + <Middle>Hi</Middle>
390 + <Bar>{'Foo'}</Bar>
391 + <Middle>There</Middle>
392 + </div>
393 + );
394 +
395 function Foo(props) {
396 ops.push('Foo');
397 return (
398 <div>
391 - <Bar>{props.text}</Bar>
399 + <Bar>{props.text2}</Bar>
400 <div hidden={true}>
401 {
402 props.step === 0 ?
395 - <div>
396 - <Middle>Hi</Middle>
397 - <Bar>{props.text}</Bar>
398 - <Middle>There</Middle>
399 - </div>
403 + step0
404 : middleContent
405 }
406 </div>
@@ -405,16 +409,29 @@ describe('ReactIncremental', function() {
409 }
410
411 // Init
408 - ReactNoop.render(<Foo text="foo" step={0} />);
412 + ReactNoop.render(<Foo text="foo" text2="foo" step={0} />);
413 + ReactNoop.flushLowPri(55);
414 +
415 + // We only finish the higher priority work. So the low pri content
416 + // has not yet finished mounting.
417 + expect(ops).toEqual(['Foo', 'Bar', 'Middle', 'Bar']);
418 +
419 + ops = [];
420 +
421 + // Interupt the rendering with a quick update. This should not touch the
422 + // middle content.
423 + ReactNoop.render(<Foo text="foo" text2="bar" step={0} />);
424 ReactNoop.flush();
425
411 - expect(ops).toEqual(['Foo', 'Bar', 'Middle', 'Bar', 'Middle']);
426 + // We've now rendered the entire tree but we didn't have to redo the work
427 + // done by the first Middle and Bar already.
428 + expect(ops).toEqual(['Foo', 'Bar', 'Middle']);
429
430 ops = [];
431
432 // Make a quick update which will schedule low priority work to
433 // update the middle content.
417 - ReactNoop.render(<Foo text="bar" step={1} />);
434 + ReactNoop.render(<Foo text="bar" text2="bar" step={1} />);
435 ReactNoop.flushLowPri(30);
436
437 expect(ops).toEqual(['Foo', 'Bar']);
@@ -429,7 +446,7 @@ describe('ReactIncremental', function() {
446
447 // but we'll interupt it to render some higher priority work.
448 // The middle content will bailout so it remains untouched.
432 - ReactNoop.render(<Foo text="foo" step={1} />);
449 + ReactNoop.render(<Foo text="foo" text2="bar" step={1} />);
450 ReactNoop.flushLowPri(30);
451
452 expect(ops).toEqual(['Foo', 'Bar']);