@samitouri / QOS-React / commits / 4266f08e48

[Fiber] "Task" priority for error boundaries and batched updates (#8193)

* Refactor scheduling functions and introduce Task priority There was lots of duplication across all the scheduling functions. I think we're far enough along that we can start trying to clean some stuff up. Also introduces a new priority level provisionally called Task priority. This is for work that completes at the end of the current tick, after the current batch of work has been committed. It's different from Synchronous priority, which needs to complete immediately. A full implementation of Task priority will follow. It will replace the current batching solution. * Implement Task priority Task priority is similar to Synchronous priority. Both are flushed in the current tick. Synchronous priority is flushed immediately (e.g. sync work triggered by setState will flush before setState exits), where as Task is flushed after the current batch of work is committed. Currently used for batchedUpdates and nested sync updates. Task should also be used for componentDidUpdate/Mount and error boundary work. I'll add this in a later commit. * Make error boundaries use Task Priority I have all but one error fixed. Not sure how tricky the last one is, but I'm cautiously optimistic. Pushing to show my current progress. * Remove recursion from handleErrors Changed the algorithm of handleErrors a bit to ensure that boundaries are not revisited once they are acknowledged. * Add incremental error boundary test Discovered an edge case: a noop error boundary will break in incremental mode unless you explicitly schedule an update. * Refactor error boundaries in Fiber * Simplify trapError() calls The existing logic was written before we had a proper error handling system. * Remove unnecessary flags * Prevent performTaskWork recursion * Be stricter about preventing recursion

Andrew Clark committed Nov 4, 2016 at 12:40 UTC 4266f08e489748bd68e1f9e0d5e408e6819cc4ed
7 files changed +314 -369
scripts/fiber/tests-failing.txt
-3
@@ -554,9 +554,6 @@ src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js
554 src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentNestedState-test.js
555 * should provide up to date values for props
556
557 -src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js
558 -* should support setting state
559 -
557 src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js
558 * should still throw when rendering to undefined
559 * should be able to switch between rendering null and a normal tag
scripts/fiber/tests-passing.txt
+2
@@ -791,6 +791,7 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
791 * skips will/DidUpdate when bailing unless an update was already in progress
792 * performs batched updates at the end of the batch
793 * can nest batchedUpdates
794 +* propagates an error from a noop error boundary
795
796 src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
797 * handles isMounted even when the initial render is deferred
@@ -1016,6 +1017,7 @@ src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentDOMMinima
1017 * should not render extra nodes for non-interpolated text
1018
1019 src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js
1020 +* should support setting state
1021 * should batch unmounts
1022
1023 src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js
src/renderers/shared/fiber/ReactFiberCommitWork.js
+25 -51
@@ -12,7 +12,6 @@
12
13 'use strict';
14
15 -import type { TrappedError } from 'ReactFiberErrorBoundary';
15 import type { Fiber } from 'ReactFiber';
16 import type { FiberRoot } from 'ReactFiberRoot';
17 import type { HostConfig } from 'ReactFiberReconciler';
@@ -24,7 +23,6 @@ var {
23 HostComponent,
24 HostText,
25 } = ReactTypeOfWork;
27 -var { trapError } = require('ReactFiberErrorBoundary');
26 var { callCallbacks } = require('ReactFiberUpdateQueue');
27
28 var {
@@ -33,7 +31,10 @@ var {
31 Callback,
32 } = require('ReactTypeOfSideEffect');
33
36 -module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
34 +module.exports = function<T, P, I, TI, C>(
35 + config : HostConfig<T, P, I, TI, C>,
36 + trapError : (fiber : Fiber, error: Error, isUnmounting : boolean) => void
37 +) {
38
39 const updateContainer = config.updateContainer;
40 const commitUpdate = config.commitUpdate;
@@ -156,10 +157,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
157 }
158 }
159
159 - function commitNestedUnmounts(root : Fiber): Array<TrappedError> | null {
160 - // Since errors are rare, we allocate this array on demand.
161 - let trappedErrors = null;
162 -
160 + function commitNestedUnmounts(root : Fiber): void {
161 // While we're inside a removed host node we don't want to call
162 // removeChild on the inner nodes because they're removed by the top
163 // call anyway. We also want to call componentWillUnmount on all
@@ -167,58 +165,39 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
165 // we do an inner loop while we're still inside the host node.
166 let node : Fiber = root;
167 while (true) {
170 - const error = commitUnmount(node);
171 - if (error) {
172 - trappedErrors = trappedErrors || [];
173 - trappedErrors.push(error);
174 - }
168 + commitUnmount(node);
169 if (node.child) {
170 // TODO: Coroutines need to visit the stateNode.
171 node = node.child;
172 continue;
173 }
174 if (node === root) {
181 - return trappedErrors;
175 + return;
176 }
177 while (!node.sibling) {
178 if (!node.return || node.return === root) {
185 - return trappedErrors;
179 + return;
180 }
181 node = node.return;
182 }
183 node = node.sibling;
184 }
191 - return trappedErrors;
185 }
186
194 - function unmountHostComponents(parent, current): Array<TrappedError> | null {
195 - // Since errors are rare, we allocate this array on demand.
196 - let trappedErrors = null;
197 -
187 + function unmountHostComponents(parent, current): void {
188 // We only have the top Fiber that was inserted but we need recurse down its
189 // children to find all the terminal nodes.
190 let node : Fiber = current;
191 while (true) {
192 if (node.tag === HostComponent || node.tag === HostText) {
203 - const errors = commitNestedUnmounts(node);
204 - if (errors) {
205 - if (!trappedErrors) {
206 - trappedErrors = errors;
207 - } else {
208 - trappedErrors.push.apply(trappedErrors, errors);
209 - }
210 - }
193 + commitNestedUnmounts(node);
194 // After all the children have unmounted, it is now safe to remove the
195 // node from the tree.
196 if (parent) {
197 removeChild(parent, node.stateNode);
198 }
199 } else {
217 - const error = commitUnmount(node);
218 - if (error) {
219 - trappedErrors = trappedErrors || [];
220 - trappedErrors.push(error);
221 - }
200 + commitUnmount(node);
201 if (node.child) {
202 // TODO: Coroutines need to visit the stateNode.
203 node = node.child;
@@ -226,24 +205,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
205 }
206 }
207 if (node === current) {
229 - return trappedErrors;
208 + return;
209 }
210 while (!node.sibling) {
211 if (!node.return || node.return === current) {
233 - return trappedErrors;
212 + return;
213 }
214 node = node.return;
215 }
216 node = node.sibling;
217 }
239 - return trappedErrors;
218 }
219
242 - function commitDeletion(current : Fiber) : Array<TrappedError> | null {
220 + function commitDeletion(current : Fiber) : void {
221 // Recursively delete all host nodes from the parent.
222 const parent = getHostParent(current);
223 // Detach refs and call componentWillUnmount() on the whole subtree.
246 - const trappedErrors = unmountHostComponents(parent, current);
224 + unmountHostComponents(parent, current);
225
226 // Cut off the return pointers to disconnect it from the tree. Ideally, we
227 // should clear the child pointer of the parent alternate to let this
@@ -256,11 +234,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
234 current.alternate.child = null;
235 current.alternate.return = null;
236 }
259 -
260 - return trappedErrors;
237 }
238
263 - function commitUnmount(current : Fiber) : TrappedError | null {
239 + function commitUnmount(current : Fiber) : void {
240 switch (current.tag) {
241 case ClassComponent: {
242 detachRef(current);
@@ -268,17 +244,14 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
244 if (typeof instance.componentWillUnmount === 'function') {
245 const error = tryCallComponentWillUnmount(instance);
246 if (error) {
271 - return trapError(current, error);
247 + trapError(current, error, true);
248 }
249 }
274 - return null;
250 + return;
251 }
252 case HostComponent: {
253 detachRef(current);
278 - return null;
279 - }
280 - default: {
281 - return null;
254 + return;
255 }
256 }
257 }
@@ -323,7 +296,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
296 }
297 }
298
326 - function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : TrappedError | null {
299 + function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void {
300 switch (finishedWork.tag) {
301 case ClassComponent: {
302 const instance = finishedWork.stateNode;
@@ -353,9 +326,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
326 }
327 }
328 if (error) {
356 - return trapError(finishedWork, error);
329 + trapError(finishedWork, error, false);
330 }
358 - return null;
331 + return;
332 }
333 case HostContainer: {
334 const rootFiber = finishedWork.stateNode;
@@ -364,15 +337,16 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
337 rootFiber.callbackList = null;
338 callCallbacks(callbackList, rootFiber.current.child.stateNode);
339 }
340 + return;
341 }
342 case HostComponent: {
343 const instance : I = finishedWork.stateNode;
344 attachRef(current, finishedWork, instance);
371 - return null;
345 + return;
346 }
347 case HostText: {
348 // We have no life-cycles associated with text.
375 - return null;
349 + return;
350 }
351 default:
352 throw new Error('This unit of work tag should not have side-effects.');
src/renderers/shared/fiber/ReactFiberErrorBoundary.js deleted
-53
@@ -1,53 +0,0 @@
1 -/**
2 - * Copyright 2013-present, Facebook, Inc.
3 - * All rights reserved.
4 - *
5 - * This source code is licensed under the BSD-style license found in the
6 - * LICENSE file in the root directory of this source tree. An additional grant
7 - * of patent rights can be found in the PATENTS file in the same directory.
8 - *
9 - * @providesModule ReactFiberErrorBoundary
10 - * @flow
11 - */
12 -
13 -'use strict';
14 -
15 -import type { Fiber } from 'ReactFiber';
16 -
17 -var {
18 - ClassComponent,
19 -} = require('ReactTypeOfWork');
20 -
21 -export type TrappedError = {
22 - boundary: Fiber | null,
23 - error: any,
24 -};
25 -
26 -function findClosestErrorBoundary(fiber : Fiber): Fiber | null {
27 - let maybeErrorBoundary = fiber.return;
28 - while (maybeErrorBoundary) {
29 - if (maybeErrorBoundary.tag === ClassComponent) {
30 - const instance = maybeErrorBoundary.stateNode;
31 - if (typeof instance.unstable_handleError === 'function') {
32 - return maybeErrorBoundary;
33 - }
34 - }
35 - maybeErrorBoundary = maybeErrorBoundary.return;
36 - }
37 - return null;
38 -}
39 -
40 -function trapError(fiber : Fiber, error : any) : TrappedError {
41 - return {
42 - boundary: findClosestErrorBoundary(fiber),
43 - error,
44 - };
45 -}
46 -
47 -function acknowledgeErrorInBoundary(boundary : Fiber, error : any) {
48 - const instance = boundary.stateNode;
49 - instance.unstable_handleError(error);
50 -}
51 -
52 -exports.trapError = trapError;
53 -exports.acknowledgeErrorInBoundary = acknowledgeErrorInBoundary;
src/renderers/shared/fiber/ReactFiberScheduler.js
+258 -257
@@ -12,7 +12,6 @@
12
13 'use strict';
14
15 -import type { TrappedError } from 'ReactFiberErrorBoundary';
15 import type { Fiber } from 'ReactFiber';
16 import type { FiberRoot } from 'ReactFiberRoot';
17 import type { HostConfig, Deadline } from 'ReactFiberReconciler';
@@ -24,13 +23,15 @@ var ReactFiberCommitWork = require('ReactFiberCommitWork');
23 var ReactCurrentOwner = require('ReactCurrentOwner');
24
25 var { cloneFiber } = require('ReactFiber');
27 -var { trapError, acknowledgeErrorInBoundary } = require('ReactFiberErrorBoundary');
26
27 var {
28 NoWork,
31 - LowPriority,
32 - AnimationPriority,
29 SynchronousPriority,
30 + TaskPriority,
31 + AnimationPriority,
32 + HighPriority,
33 + LowPriority,
34 + OffscreenPriority,
35 } = require('ReactPriorityLevel');
36
37 var {
@@ -48,6 +49,7 @@ var {
49
50 var {
51 HostContainer,
52 + ClassComponent,
53 } = require('ReactTypeOfWork');
54
55 if (__DEV__) {
@@ -56,14 +58,19 @@ if (__DEV__) {
58
59 var timeHeuristicForUnitOfWork = 1;
60
61 +type TrappedError = {
62 + boundary: Fiber | null,
63 + error: any,
64 +};
65 +
66 module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
67 const { beginWork } = ReactFiberBeginWork(config, scheduleUpdate);
68 const { completeWork } = ReactFiberCompleteWork(config);
69 const { commitInsertion, commitDeletion, commitWork, commitLifeCycles } =
63 - ReactFiberCommitWork(config);
70 + ReactFiberCommitWork(config, trapError);
71
65 - const scheduleAnimationCallback = config.scheduleAnimationCallback;
66 - const scheduleDeferredCallback = config.scheduleDeferredCallback;
72 + const hostScheduleAnimationCallback = config.scheduleAnimationCallback;
73 + const hostScheduleDeferredCallback = config.scheduleDeferredCallback;
74 const useSyncScheduling = config.useSyncScheduling;
75
76 // The priority level to use when scheduling an update.
@@ -74,6 +81,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
81 // Whether updates should be batched. Only applies when using sync scheduling.
82 let shouldBatchUpdates : boolean = false;
83
84 + // Need this to prevent recursion while in a Task loop.
85 + let isPerformingTaskWork : boolean = false;
86 +
87 // The next work in progress fiber that we're currently working on.
88 let nextUnitOfWork : ?Fiber = null;
89 let nextPriorityLevel : PriorityLevel = NoWork;
@@ -86,6 +96,24 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
96 let isAnimationCallbackScheduled : boolean = false;
97 let isDeferredCallbackScheduled : boolean = false;
98
99 + // Caught errors and error boundaries that are currently handling them
100 + let activeErrorBoundaries : Set<Fiber> | null = null;
101 + let nextTrappedErrors : Array<TrappedError> | null = null;
102 +
103 + function scheduleAnimationCallback(callback) {
104 + if (!isAnimationCallbackScheduled) {
105 + isAnimationCallbackScheduled = true;
106 + hostScheduleAnimationCallback(callback);
107 + }
108 + }
109 +
110 + function scheduleDeferredCallback(callback) {
111 + if (!isDeferredCallbackScheduled) {
112 + isDeferredCallbackScheduled = true;
113 + hostScheduleDeferredCallback(callback);
114 + }
115 + }
116 +
117 function findNextUnitOfWork() {
118 // Clear out roots with no more work on them.
119 while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
@@ -131,15 +159,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
159 return null;
160 }
161
134 - function commitAllWork(finishedWork : Fiber, ignoreUnmountingErrors : boolean) {
162 + function commitAllWork(finishedWork : Fiber) {
163 // Commit all the side-effects within a tree.
136 -
137 - // Commit phase is meant to be atomic and non-interruptible.
138 - // Any errors raised in it should be handled after it is over
139 - // so that we don't end up in an inconsistent state due to user code.
140 - // We'll keep track of all caught errors and handle them later.
141 - let allTrappedErrors = null;
142 -
164 // First, we'll perform all the host insertions, updates, deletions and
165 // ref unmounts.
166 let effectfulFiber = finishedWork.firstEffect;
@@ -150,7 +171,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
171 commitInsertion(effectfulFiber);
172 // Clear the "placement" from effect tag so that we know that this is inserted, before
173 // any life-cycles like componentDidMount gets called.
153 - effectfulFiber.effectTag = NoEffect;
174 + effectfulFiber.effectTag ^= Placement;
175 break;
176 }
177 case PlacementAndUpdate:
@@ -159,7 +180,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
180 commitInsertion(effectfulFiber);
181 // Clear the "placement" from effect tag so that we know that this is inserted, before
182 // any life-cycles like componentDidMount gets called.
162 - effectfulFiber.effectTag = Update;
183 + effectfulFiber.effectTag ^= Placement;
184
185 // Update
186 const current = effectfulFiber.alternate;
@@ -173,20 +194,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
194 break;
195 case Deletion:
196 case DeletionAndCallback:
176 - // Deletion might cause an error in componentWillUnmount().
177 - // We will continue nevertheless and handle those later on.
178 - const trappedErrors = commitDeletion(effectfulFiber);
179 - // There is a special case where we completely ignore errors.
180 - // It happens when we already caught an error earlier, and the update
181 - // is caused by an error boundary trying to render an error message.
182 - // In this case, we want to blow away the tree without catching errors.
183 - if (trappedErrors && !ignoreUnmountingErrors) {
184 - if (!allTrappedErrors) {
185 - allTrappedErrors = trappedErrors;
186 - } else {
187 - allTrappedErrors.push.apply(allTrappedErrors, trappedErrors);
188 - }
189 - }
197 + commitDeletion(effectfulFiber);
198 break;
199 }
200
@@ -200,11 +208,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
208 while (effectfulFiber) {
209 if (effectfulFiber.effectTag & (Update | Callback)) {
210 const current = effectfulFiber.alternate;
203 - const trappedError = commitLifeCycles(current, effectfulFiber);
204 - if (trappedError) {
205 - allTrappedErrors = allTrappedErrors || [];
206 - allTrappedErrors.push(trappedError);
207 - }
211 + commitLifeCycles(current, effectfulFiber);
212 }
213 const next = effectfulFiber.nextEffect;
214 // Ensure that we clean these up so that we don't accidentally keep them.
@@ -222,17 +226,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
226 if (finishedWork.effectTag !== NoEffect) {
227 const current = finishedWork.alternate;
228 commitWork(current, finishedWork);
225 - const trappedError = commitLifeCycles(current, finishedWork);
226 - if (trappedError) {
227 - allTrappedErrors = allTrappedErrors || [];
228 - allTrappedErrors.push(trappedError);
229 - }
229 + commitLifeCycles(current, finishedWork);
230 }
231
232 - // Now that the tree has been committed, we can handle errors.
233 - if (allTrappedErrors) {
234 - handleErrors(allTrappedErrors);
235 - }
232 + // The task work includes batched updates and error handling.
233 + performTaskWork();
234 }
235
236 function resetWorkPriority(workInProgress : Fiber) {
@@ -253,7 +251,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
251 workInProgress.pendingWorkPriority = newPriority;
252 }
253
256 - function completeUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
254 + function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
255 while (true) {
256 // The current, flushed, state of this fiber is the alternate.
257 // Ideally nothing should rely on this, but relying on it here
@@ -325,7 +323,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
323 // "next" scheduled work since we've already scanned passed. That
324 // also ensures that work scheduled during reconciliation gets deferred.
325 // const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
328 - commitAllWork(workInProgress, ignoreUnmountingErrors);
326 + commitAllWork(workInProgress);
327 const nextWork = findNextUnitOfWork();
328 // if (!nextWork && hasMoreWork) {
329 // TODO: This can happen when some deep work completes and we don't
@@ -339,7 +337,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
337 }
338 }
339
342 - function performUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
340 + function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
341 // The current, flushed, state of this fiber is the alternate.
342 // Ideally nothing should rely on this, but relying on it here
343 // means that we don't need an additional field on the work in
@@ -360,7 +358,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
358 ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress);
359 }
360 // If this doesn't spawn new work, complete the current work.
363 - next = completeUnitOfWork(workInProgress, ignoreUnmountingErrors);
361 + next = completeUnitOfWork(workInProgress);
362 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
363 ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress);
364 }
@@ -377,16 +375,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
375 }
376 while (nextUnitOfWork) {
377 if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
380 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
378 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
379 if (!nextUnitOfWork) {
380 // Find more work. We might have time to complete some more.
381 nextUnitOfWork = findNextUnitOfWork();
382 }
383 } else {
386 - if (!isDeferredCallbackScheduled) {
387 - isDeferredCallbackScheduled = true;
388 - scheduleDeferredCallback(performDeferredWork);
389 - }
384 + scheduleDeferredCallback(performDeferredWork);
385 return;
386 }
387 }
@@ -397,56 +392,19 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
392 performAndHandleErrors(LowPriority, deadline);
393 }
394
400 - function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) {
401 - // We must reset the current unit of work pointer so that we restart the
402 - // search from the root during the next tick, in case there is now higher
403 - // priority work somewhere earlier than before.
404 - if (priority <= nextPriorityLevel) {
405 - nextUnitOfWork = null;
406 - }
407 -
408 - // Set the priority on the root, without deprioritizing
409 - if (root.current.pendingWorkPriority === NoWork ||
410 - priority <= root.current.pendingWorkPriority) {
411 - root.current.pendingWorkPriority = priority;
412 - }
413 -
414 - if (!root.isScheduled) {
415 - root.isScheduled = true;
416 - if (lastScheduledRoot) {
417 - // Schedule ourselves to the end.
418 - lastScheduledRoot.nextScheduledRoot = root;
419 - lastScheduledRoot = root;
420 - } else {
421 - // We're the only work scheduled.
422 - nextScheduledRoot = root;
423 - lastScheduledRoot = root;
424 - }
425 - }
426 -
427 - if (!isDeferredCallbackScheduled) {
428 - isDeferredCallbackScheduled = true;
429 - scheduleDeferredCallback(performDeferredWork);
430 - }
431 - }
432 -
395 function performAnimationWorkUnsafe() {
396 // Always start from the root
397 nextUnitOfWork = findNextUnitOfWork();
398 while (nextUnitOfWork &&
437 - nextPriorityLevel !== NoWork &&
438 - nextPriorityLevel <= AnimationPriority) {
439 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
399 + nextPriorityLevel === AnimationPriority) {
400 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
401 if (!nextUnitOfWork) {
402 // Keep searching for animation work until there's no more left
403 nextUnitOfWork = findNextUnitOfWork();
404 }
405 }
445 - if (nextUnitOfWork && nextPriorityLevel > AnimationPriority) {
446 - if (!isDeferredCallbackScheduled) {
447 - isDeferredCallbackScheduled = true;
448 - scheduleDeferredCallback(performDeferredWork);
449 - }
406 + if (nextUnitOfWork) {
407 + scheduleCallbackAtPriority(nextPriorityLevel);
408 }
409 }
410
@@ -455,81 +413,18 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
413 performAndHandleErrors(AnimationPriority);
414 }
415
458 - function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) {
459 - // Set the priority on the root, without deprioritizing
460 - if (root.current.pendingWorkPriority === NoWork ||
461 - priorityLevel <= root.current.pendingWorkPriority) {
462 - root.current.pendingWorkPriority = priorityLevel;
463 - }
464 -
465 - if (!root.isScheduled) {
466 - root.isScheduled = true;
467 - if (lastScheduledRoot) {
468 - // Schedule ourselves to the end.
469 - lastScheduledRoot.nextScheduledRoot = root;
470 - lastScheduledRoot = root;
471 - } else {
472 - // We're the only work scheduled.
473 - nextScheduledRoot = root;
474 - lastScheduledRoot = root;
475 - }
476 - }
477 -
478 - if (!isAnimationCallbackScheduled) {
479 - isAnimationCallbackScheduled = true;
480 - scheduleAnimationCallback(performAnimationWork);
481 - }
482 - }
483 -
484 - function scheduleErrorBoundaryWork(boundary : Fiber, priority) : FiberRoot {
485 - let root = null;
486 - let fiber = boundary;
487 - while (fiber) {
488 - fiber.pendingWorkPriority = priority;
489 - if (fiber.alternate) {
490 - fiber.alternate.pendingWorkPriority = priority;
491 - }
492 - if (!fiber.return) {
493 - if (fiber.tag === HostContainer) {
494 - // We found the root.
495 - // Remember it so we can update it.
496 - root = ((fiber.stateNode : any) : FiberRoot);
497 - break;
498 - } else {
499 - throw new Error('Invalid root');
500 - }
501 - }
502 - fiber = fiber.return;
503 - }
504 - if (!root) {
505 - throw new Error('Could not find root from the boundary.');
506 - }
507 - return root;
508 - }
509 -
416 function performSynchronousWorkUnsafe() {
511 - // Perform work now
417 nextUnitOfWork = findNextUnitOfWork();
418 while (nextUnitOfWork &&
419 nextPriorityLevel === SynchronousPriority) {
515 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
420 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
421
422 if (!nextUnitOfWork) {
423 nextUnitOfWork = findNextUnitOfWork();
424 }
425 }
426 if (nextUnitOfWork) {
522 - if (nextPriorityLevel > AnimationPriority) {
523 - if (!isDeferredCallbackScheduled) {
524 - isDeferredCallbackScheduled = true;
525 - scheduleDeferredCallback(performDeferredWork);
526 - }
527 - return;
528 - }
529 - if (!isAnimationCallbackScheduled) {
530 - isAnimationCallbackScheduled = true;
531 - scheduleAnimationCallback(performAnimationWork);
532 - }
427 + scheduleCallbackAtPriority(nextPriorityLevel);
428 }
429 }
430
@@ -544,148 +439,255 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
439 }
440 }
441
547 - function scheduleSynchronousWork(root : FiberRoot) {
548 - root.current.pendingWorkPriority = SynchronousPriority;
549 -
550 - if (root.isScheduled) {
551 - // If we're already scheduled, we can bail out.
552 - return;
442 + function performTaskWorkUnsafe() {
443 + if (isPerformingTaskWork) {
444 + throw new Error('Already performing task work');
445 }
554 - root.isScheduled = true;
555 - if (lastScheduledRoot) {
556 - // Schedule ourselves to the end.
557 - lastScheduledRoot.nextScheduledRoot = root;
558 - lastScheduledRoot = root;
559 - } else {
560 - // We're the only work scheduled.
561 - nextScheduledRoot = root;
562 - lastScheduledRoot = root;
446
564 - if (!shouldBatchUpdates) {
565 - // Unless in batched mode, perform work immediately
566 - performSynchronousWork();
447 + isPerformingTaskWork = true;
448 + nextUnitOfWork = findNextUnitOfWork();
449 + while (nextUnitOfWork &&
450 + nextPriorityLevel === TaskPriority) {
451 + nextUnitOfWork =
452 + performUnitOfWork(nextUnitOfWork);
453 +
454 + if (!nextUnitOfWork) {
455 + nextUnitOfWork = findNextUnitOfWork();
456 }
457 }
458 + if (nextUnitOfWork) {
459 + scheduleCallbackAtPriority(nextPriorityLevel);
460 + }
461 + isPerformingTaskWork = false;
462 + }
463 +
464 + function performTaskWork() {
465 + performAndHandleErrors(TaskPriority);
466 }
467
468 function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : null | Deadline) {
469 // The exact priority level doesn't matter, so long as it's in range of the
470 // work (sync, animation, deferred) being performed.
471 try {
575 - if (priorityLevel === SynchronousPriority) {
576 - performSynchronousWorkUnsafe();
577 - } else if (priorityLevel > AnimationPriority) {
578 - if (!deadline) {
579 - throw new Error('No deadline');
580 - } else {
581 - performDeferredWorkUnsafe(deadline);
582 - }
583 - return;
584 - } else {
585 - performAnimationWorkUnsafe();
472 + switch (priorityLevel) {
473 + case SynchronousPriority:
474 + performSynchronousWorkUnsafe();
475 + break;
476 + case TaskPriority:
477 + if (!isPerformingTaskWork) {
478 + performTaskWorkUnsafe();
479 + }
480 + break;
481 + case AnimationPriority:
482 + performAnimationWorkUnsafe();
483 + break;
484 + case HighPriority:
485 + case LowPriority:
486 + case OffscreenPriority:
487 + if (!deadline) {
488 + throw new Error('No deadline');
489 + } else {
490 + performDeferredWorkUnsafe(deadline);
491 + }
492 + break;
493 + default:
494 + break;
495 }
496 } catch (error) {
588 - const failedUnitOfWork = nextUnitOfWork;
589 - // Reset because it points to the error boundary:
590 - nextUnitOfWork = null;
591 - if (!failedUnitOfWork) {
592 - // We shouldn't end up here because nextUnitOfWork
593 - // should always be set while work is being performed.
594 - throw error;
595 - }
596 - const trappedError = trapError(failedUnitOfWork, error);
597 - handleErrors([trappedError]);
497 + trapError(nextUnitOfWork, error, false);
498 + }
499 +
500 + // If there were errors and we aren't already handling them, handle them now
501 + if (nextTrappedErrors && !activeErrorBoundaries) {
502 + handleErrors();
503 }
504 }
505
601 - function handleErrors(initialTrappedErrors : Array<TrappedError>) : void {
602 - let nextTrappedErrors = initialTrappedErrors;
603 - let firstUncaughtError = null;
506 + function handleErrors() : void {
507 + if (activeErrorBoundaries) {
508 + throw new Error('Already handling errors');
509 + }
510
605 - // In each phase, we will attempt to pass errors to boundaries and re-render them.
606 - // If we get more errors, we propagate them to higher boundaries in the next iterations.
607 - while (nextTrappedErrors) {
608 - const trappedErrors = nextTrappedErrors;
609 - nextTrappedErrors = null;
511 + // Start tracking active boundaries.
512 + activeErrorBoundaries = new Set();
513
611 - // Pass errors to all affected boundaries.
612 - const affectedBoundaries : Set<Fiber> = new Set();
613 - trappedErrors.forEach(trappedError => {
514 + // If we find unhandled errors, we'll only rethrow the first one.
515 + let firstUncaughtError = null;
516 +
517 + // All work created by error boundaries should have Task priority
518 + // so that it finishes before this function exits.
519 + const previousPriorityContext = priorityContext;
520 + priorityContext = TaskPriority;
521 +
522 + // Keep looping until there are no more trapped errors, or until we find
523 + // an unhandled error.
524 + while (nextTrappedErrors && nextTrappedErrors.length && !firstUncaughtError) {
525 + // First, find all error boundaries and notify them about errors.
526 + while (nextTrappedErrors && nextTrappedErrors.length) {
527 + const trappedError = nextTrappedErrors.shift();
528 const boundary = trappedError.boundary;
529 const error = trappedError.error;
530 if (!boundary) {
531 firstUncaughtError = firstUncaughtError || error;
618 - return;
532 + continue;
533 }
534 // Don't visit boundaries twice.
621 - if (affectedBoundaries.has(boundary)) {
622 - return;
623 - }
624 - // Give error boundary a chance to update its state.
625 - try {
626 - acknowledgeErrorInBoundary(boundary, error);
627 - affectedBoundaries.add(boundary);
628 - } catch (nextError) {
629 - // If it throws, propagate the error.
630 - nextTrappedErrors = nextTrappedErrors || [];
631 - nextTrappedErrors.push(trapError(boundary, nextError));
535 + if (activeErrorBoundaries.has(boundary)) {
536 + continue;
537 }
633 - });
634 -
635 - // We will process an update caused by each error boundary synchronously.
636 - affectedBoundaries.forEach(boundary => {
637 - const priority = priorityContext;
638 - const root = scheduleErrorBoundaryWork(boundary, priority);
639 - // This should use findNextUnitOfWork() when synchronous scheduling is implemented.
640 - let fiber = cloneFiber(root.current, priority);
538 try {
642 - while (fiber) {
643 - // TODO: this is the only place where we recurse and it's unfortunate.
644 - // (This may potentially get us into handleErrors() again.)
645 - fiber = performUnitOfWork(fiber, true);
646 - }
539 + // This error boundary is now active.
540 + // We will let it handle the error and retry rendering.
541 + // If it fails again, the next error will be propagated to the parent
542 + // boundary or rethrown.
543 + activeErrorBoundaries.add(boundary);
544 + // Give error boundary a chance to update its state.
545 + // Updates will be scheduled with Task priority.
546 + const instance = boundary.stateNode;
547 + instance.unstable_handleError(error);
548 +
549 + // Schedule an update, in case the boundary didn't call setState
550 + // on itself.
551 + scheduleUpdate(boundary);
552 } catch (nextError) {
648 - // If it throws, propagate the error.
649 - nextTrappedErrors = nextTrappedErrors || [];
650 - nextTrappedErrors.push(trapError(boundary, nextError));
553 + // If an error is thrown, propagate the error to the parent boundary.
554 + trapError(boundary, nextError, false);
555 }
652 - });
556 + }
557 +
558 + // Now that we attempt to flush any work that was scheduled by the boundaries.
559 + // If this creates errors, they will be pushed to nextTrappedErrors and
560 + // the outer loop will continue.
561 + try {
562 + performTaskWorkUnsafe();
563 + } catch (error) {
564 + isPerformingTaskWork = false;
565 + trapError(nextUnitOfWork, error, false);
566 + }
567 }
568
655 - ReactCurrentOwner.current = null;
569 + nextTrappedErrors = null;
570 + activeErrorBoundaries = null;
571 + priorityContext = previousPriorityContext;
572
657 - // Surface the first error uncaught by the boundaries to the user.
573 if (firstUncaughtError) {
574 // We need to make sure any future root can get scheduled despite these errors.
575 // Currently after throwing, nothing gets scheduled because these fields are set.
576 // FIXME: this is likely a wrong fix! It's still better than ignoring updates though.
577 nextScheduledRoot = null;
578 lastScheduledRoot = null;
664 -
665 - // Throw any unhandled errors.
579 throw firstUncaughtError;
580 }
581 }
582
583 + function findClosestErrorBoundary(fiber : Fiber): Fiber | null {
584 + let maybeErrorBoundary = fiber.return;
585 + while (maybeErrorBoundary) {
586 + if (maybeErrorBoundary.tag === ClassComponent) {
587 + const instance = maybeErrorBoundary.stateNode;
588 + const isErrorBoundary = typeof instance.unstable_handleError === 'function';
589 + if (isErrorBoundary) {
590 + const isHandlingAnotherError = (
591 + activeErrorBoundaries !== null &&
592 + activeErrorBoundaries.has(maybeErrorBoundary)
593 + );
594 + if (!isHandlingAnotherError) {
595 + return maybeErrorBoundary;
596 + }
597 + }
598 + }
599 + maybeErrorBoundary = maybeErrorBoundary.return;
600 + }
601 + return null;
602 + }
603 +
604 + function trapError(fiber : Fiber | null, error : any, isUnmounting : boolean) : void {
605 + // It is no longer valid because we exited the user code.
606 + ReactCurrentOwner.current = null;
607 +
608 + if (isUnmounting && activeErrorBoundaries) {
609 + // Ignore errors caused by unmounting during error handling.
610 + // This lets error boundaries safely tear down already failed trees.
611 + return;
612 + }
613 +
614 + if (!nextTrappedErrors) {
615 + nextTrappedErrors = [];
616 + }
617 + nextTrappedErrors.push({
618 + boundary: fiber ? findClosestErrorBoundary(fiber) : null,
619 + error,
620 + });
621 + }
622 +
623 function scheduleWork(root : FiberRoot) {
671 - scheduleWorkAtPriority(root, priorityContext);
624 + let priorityLevel = priorityContext;
625 +
626 + // If we're in a batch, switch to task priority
627 + if (priorityLevel === SynchronousPriority && shouldBatchUpdates) {
628 + priorityLevel = TaskPriority;
629 + }
630 +
631 + scheduleWorkAtPriority(root, priorityLevel);
632 }
633
634 function scheduleWorkAtPriority(root : FiberRoot, priorityLevel : PriorityLevel) {
675 - if (priorityLevel === NoWork) {
676 - return;
677 - } else if (priorityLevel === SynchronousPriority) {
678 - scheduleSynchronousWork(root);
679 - } else if (priorityLevel <= AnimationPriority) {
680 - scheduleAnimationWork(root, priorityLevel);
681 - } else {
682 - scheduleDeferredWork(root, priorityLevel);
683 - return;
635 + // Set the priority on the root, without deprioritizing
636 + if (root.current.pendingWorkPriority === NoWork ||
637 + priorityLevel <= root.current.pendingWorkPriority) {
638 + root.current.pendingWorkPriority = priorityLevel;
639 + }
640 +
641 + if (!root.isScheduled) {
642 + root.isScheduled = true;
643 + if (lastScheduledRoot) {
644 + // Schedule ourselves to the end.
645 + lastScheduledRoot.nextScheduledRoot = root;
646 + lastScheduledRoot = root;
647 + } else {
648 + // We're the only work scheduled.
649 + nextScheduledRoot = root;
650 + lastScheduledRoot = root;
651 + }
652 + }
653 +
654 + // We must reset the current unit of work pointer so that we restart the
655 + // search from the root during the next tick, in case there is now higher
656 + // priority work somewhere earlier than before.
657 + if (priorityLevel <= nextPriorityLevel) {
658 + nextUnitOfWork = null;
659 + }
660 +
661 + scheduleCallbackAtPriority(priorityLevel);
662 + }
663 +
664 + function scheduleCallbackAtPriority(priorityLevel : PriorityLevel) {
665 + switch (priorityLevel) {
666 + case SynchronousPriority:
667 + // Perform work immediately
668 + performSynchronousWork();
669 + return;
670 + case TaskPriority:
671 + // Do nothing. Task work should be flushed after committing.
672 + return;
673 + case AnimationPriority:
674 + scheduleAnimationCallback(performAnimationWork);
675 + return;
676 + case HighPriority:
677 + case LowPriority:
678 + case OffscreenPriority:
679 + scheduleDeferredCallback(performDeferredWork);
680 + return;
681 }
682 }
683
684 function scheduleUpdate(fiber : Fiber) {
688 - const priorityLevel = priorityContext;
685 + let priorityLevel = priorityContext;
686 + // If we're in a batch, switch to task priority
687 + if (priorityLevel === SynchronousPriority && shouldBatchUpdates) {
688 + priorityLevel = TaskPriority;
689 + }
690 +
691 while (true) {
692 if (fiber.pendingWorkPriority === NoWork ||
693 fiber.pendingWorkPriority >= priorityLevel) {
@@ -727,9 +729,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
729 return fn();
730 } finally {
731 shouldBatchUpdates = prev;
730 - // If we've exited the batch, perform any scheduled sync work
732 + // If we've exited the batch, perform any scheduled task work
733 if (!shouldBatchUpdates) {
732 - performSynchronousWork();
734 + performTaskWork();
735 }
736 }
737 }
@@ -746,7 +748,6 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
748
749 return {
750 scheduleWork: scheduleWork,
749 - scheduleDeferredWork: scheduleDeferredWork,
751 performWithPriority: performWithPriority,
752 batchedUpdates: batchedUpdates,
753 syncUpdates: syncUpdates,
src/renderers/shared/fiber/ReactPriorityLevel.js
+6 -5
@@ -12,13 +12,14 @@
12
13 'use strict';
14
15 -export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
15 +export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6;
16
17 module.exports = {
18 NoWork: 0, // No work is pending.
19 SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
20 - AnimationPriority: 2, // Needs to complete before the next frame.
21 - HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
22 - LowPriority: 4, // Data fetching, or result from updating stores.
23 - OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
20 + TaskPriority: 2, // Completes at the end of the current tick.
21 + AnimationPriority: 3, // Needs to complete before the next frame.
22 + HighPriority: 4, // Interaction that needs to complete pretty soon to feel responsive.
23 + LowPriority: 5, // Data fetching, or result from updating stores.
24 + OffscreenPriority: 6, // Won't be visible but do the work in case it becomes visible.
25 };
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+23
@@ -1465,4 +1465,27 @@ describe('ReactIncremental', () => {
1465 ]);
1466 expect(instance.state.n).toEqual(4);
1467 });
1468 +
1469 + it('propagates an error from a noop error boundary', () => {
1470 + class NoopBoundary extends React.Component {
1471 + unstable_handleError() {
1472 + // Noop
1473 + }
1474 + render() {
1475 + return this.props.children;
1476 + }
1477 + }
1478 +
1479 + function RenderError() {
1480 + throw new Error('render error');
1481 + }
1482 +
1483 + ReactNoop.render(
1484 + <NoopBoundary>
1485 + <RenderError />
1486 + </NoopBoundary>
1487 + );
1488 +
1489 + expect(ReactNoop.flush).toThrow('render error');
1490 + });
1491 });