react-native
Version:
A framework for building native apps using React
1,240 lines (1,102 loc) • 44.2 kB
JavaScript
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactFiberScheduler
* @flow
*/
'use strict';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig, Deadline } from 'ReactFiberReconciler';
import type { PriorityLevel } from 'ReactPriorityLevel';
export type CapturedError = {
componentName : ?string,
componentStack : string,
error : Error,
errorBoundary : ?Object,
errorBoundaryFound : boolean,
errorBoundaryName : string | null,
willRetry : boolean,
};
var {
popContextProvider,
} = require('ReactFiberContext');
const { reset } = require('ReactFiberStack');
var {
getStackAddendumByWorkInProgressFiber,
} = require('react/lib/ReactComponentTreeHook');
var { logCapturedError } = require('ReactFiberErrorLogger');
var ReactFiberBeginWork = require('ReactFiberBeginWork');
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
var ReactFiberCommitWork = require('ReactFiberCommitWork');
var ReactFiberHostContext = require('ReactFiberHostContext');
var ReactCurrentOwner = require('react/lib/ReactCurrentOwner');
var ReactFeatureFlags = require('ReactFeatureFlags');
var getComponentName = require('getComponentName');
var { cloneFiber } = require('ReactFiber');
var { onCommitRoot } = require('ReactFiberDevToolsHook');
var {
NoWork,
SynchronousPriority,
TaskPriority,
AnimationPriority,
HighPriority,
LowPriority,
OffscreenPriority,
} = require('ReactPriorityLevel');
var {
NoEffect,
Placement,
Update,
PlacementAndUpdate,
Deletion,
ContentReset,
Callback,
Err,
Ref,
} = require('ReactTypeOfSideEffect');
var {
HostRoot,
HostComponent,
HostPortal,
ClassComponent,
} = require('ReactTypeOfWork');
var {
getPendingPriority,
} = require('ReactFiberUpdateQueue');
var {
resetContext,
} = require('ReactFiberContext');
var invariant = require('fbjs/lib/invariant');
if (__DEV__) {
var warning = require('fbjs/lib/warning');
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
var warnAboutUpdateOnUnmounted = function(instance : ReactClass<any>) {
const ctor = instance.constructor;
warning(
false,
'Can only update a mounted or mounting component. This usually means ' +
'you called setState, replaceState, or forceUpdate on an unmounted ' +
'component. This is a no-op.\n\nPlease check the code for the ' +
'%s component.',
ctor && (ctor.displayName || ctor.name) || 'ReactClass'
);
};
}
var timeHeuristicForUnitOfWork = 1;
module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P, I, TI, PI, C, CX, PL>) {
const hostContext = ReactFiberHostContext(config);
const { popHostContainer, popHostContext, resetHostContainer } = hostContext;
const { beginWork, beginFailedWork } = ReactFiberBeginWork(
config,
hostContext,
scheduleUpdate,
getPriorityContext,
);
const { completeWork } = ReactFiberCompleteWork(config, hostContext);
const {
commitPlacement,
commitDeletion,
commitWork,
commitLifeCycles,
commitRef,
} = ReactFiberCommitWork(config, captureError);
const {
scheduleAnimationCallback: hostScheduleAnimationCallback,
scheduleDeferredCallback: hostScheduleDeferredCallback,
useSyncScheduling,
prepareForCommit,
resetAfterCommit,
} = config;
// The priority level to use when scheduling an update.
// TODO: Should we change this to an array? Might be less confusing.
let priorityContext : PriorityLevel = useSyncScheduling ?
SynchronousPriority :
LowPriority;
// Keep track of this so we can reset the priority context if an error
// is thrown during reconciliation.
let priorityContextBeforeReconciliation : PriorityLevel = NoWork;
// Keeps track of whether we're currently in a work loop.
let isPerformingWork : boolean = false;
// Keeps track of whether we should should batch sync updates.
let isBatchingUpdates : boolean = false;
// The next work in progress fiber that we're currently working on.
let nextUnitOfWork : Fiber | null = null;
let nextPriorityLevel : PriorityLevel = NoWork;
// The next fiber with an effect that we're currently committing.
let nextEffect : Fiber | null = null;
let pendingCommit : Fiber | null = null;
// Linked list of roots with scheduled work on them.
let nextScheduledRoot : FiberRoot | null = null;
let lastScheduledRoot : FiberRoot | null = null;
// Keep track of which host environment callbacks are scheduled.
let isAnimationCallbackScheduled : boolean = false;
let isDeferredCallbackScheduled : boolean = false;
// Keep track of which fibers have captured an error that need to be handled.
// Work is removed from this collection after unstable_handleError is called.
let capturedErrors : Map<Fiber, CapturedError> | null = null;
// Keep track of which fibers have failed during the current batch of work.
// This is a different set than capturedErrors, because it is not reset until
// the end of the batch. This is needed to propagate errors correctly if a
// subtree fails more than once.
let failedBoundaries : Set<Fiber> | null = null;
// Error boundaries that captured an error during the current commit.
let commitPhaseBoundaries : Set<Fiber> | null = null;
let firstUncaughtError : Error | null = null;
let fatalError : Error | null = null;
let isCommitting : boolean = false;
let isUnmounting : boolean = false;
function scheduleAnimationCallback(callback) {
if (!isAnimationCallbackScheduled) {
isAnimationCallbackScheduled = true;
hostScheduleAnimationCallback(callback);
}
}
function scheduleDeferredCallback(callback) {
if (!isDeferredCallbackScheduled) {
isDeferredCallbackScheduled = true;
hostScheduleDeferredCallback(callback);
}
}
function resetContextStack() {
// Reset the stack
reset();
// Reset the cursors
resetContext();
resetHostContainer();
}
// findNextUnitOfWork mutates the current priority context. It is reset after
// after the workLoop exits, so never call findNextUnitOfWork from outside
// the work loop.
function findNextUnitOfWork() {
// Clear out roots with no more work on them, or if they have uncaught errors
while (nextScheduledRoot !== null && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
// Unschedule this root.
nextScheduledRoot.isScheduled = false;
// Read the next pointer now.
// We need to clear it in case this root gets scheduled again later.
const next = nextScheduledRoot.nextScheduledRoot;
nextScheduledRoot.nextScheduledRoot = null;
// Exit if we cleared all the roots and there's no work to do.
if (nextScheduledRoot === lastScheduledRoot) {
nextScheduledRoot = null;
lastScheduledRoot = null;
nextPriorityLevel = NoWork;
return null;
}
// Continue with the next root.
// If there's no work on it, it will get unscheduled too.
nextScheduledRoot = next;
}
let root = nextScheduledRoot;
let highestPriorityRoot = null;
let highestPriorityLevel = NoWork;
while (root !== null) {
if (root.current.pendingWorkPriority !== NoWork && (
highestPriorityLevel === NoWork ||
highestPriorityLevel > root.current.pendingWorkPriority)) {
highestPriorityLevel = root.current.pendingWorkPriority;
highestPriorityRoot = root;
}
// We didn't find anything to do in this root, so let's try the next one.
root = root.nextScheduledRoot;
}
if (highestPriorityRoot !== null) {
nextPriorityLevel = highestPriorityLevel;
priorityContext = nextPriorityLevel;
// Before we start any new work, let's make sure that we have a fresh
// stack to work from.
// TODO: This call is buried a bit too deep. It would be nice to have
// a single point which happens right before any new work and
// unfortunately this is it.
resetContextStack();
return cloneFiber(
highestPriorityRoot.current,
highestPriorityLevel
);
}
nextPriorityLevel = NoWork;
return null;
}
function commitAllHostEffects() {
while (nextEffect !== null) {
if (__DEV__) {
ReactDebugCurrentFiber.current = nextEffect;
}
if (nextEffect.effectTag & ContentReset) {
config.resetTextContent(nextEffect.stateNode);
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
let primaryEffectTag =
nextEffect.effectTag & ~(Callback | Err | ContentReset | Ref);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted
// does and isMounted is deprecated anyway so we should be able
// to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
isUnmounting = true;
commitDeletion(nextEffect);
isUnmounting = false;
break;
}
}
nextEffect = nextEffect.nextEffect;
}
if (__DEV__) {
ReactDebugCurrentFiber.current = null;
}
}
function commitAllLifeCycles() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
// Use Task priority for lifecycle updates
if (nextEffect.effectTag & (Update | Callback)) {
commitLifeCycles(current, nextEffect);
}
if (nextEffect.effectTag & Ref) {
commitRef(nextEffect);
}
if (nextEffect.effectTag & Err) {
commitErrorHandling(nextEffect);
}
const next = nextEffect.nextEffect;
// Ensure that we clean these up so that we don't accidentally keep them.
// I'm not actually sure this matters because we can't reset firstEffect
// and lastEffect since they're on every node, not just the effectful
// ones. So we have to clean everything as we reuse nodes anyway.
nextEffect.nextEffect = null;
// Ensure that we reset the effectTag here so that we can rely on effect
// tags to reason about the current life-cycle.
nextEffect = next;
}
}
function commitAllWork(finishedWork : Fiber) {
// We keep track of this so that captureError can collect any boundaries
// that capture an error during the commit phase. The reason these aren't
// local to this function is because errors that occur during cWU are
// captured elsewhere, to prevent the unmount from being interrupted.
isCommitting = true;
pendingCommit = null;
const root : FiberRoot = (finishedWork.stateNode : any);
invariant(
root.current !== finishedWork,
'Cannot commit the same tree as before. This is probably a bug ' +
'related to the return field. This error is likely caused by a bug ' +
'in React. Please file an issue.'
);
// Reset this to null before calling lifecycles
ReactCurrentOwner.current = null;
// Updates that occur during the commit phase should have Task priority
const previousPriorityContext = priorityContext;
priorityContext = TaskPriority;
let firstEffect;
if (finishedWork.effectTag !== NoEffect) {
// A fiber's effect list consists only of its children, not itself. So if
// the root has an effect, we need to add it to the end of the list. The
// resulting list is the set that would belong to the root's parent, if
// it had one; that is, all the effects in the tree including the root.
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}
const commitInfo = prepareForCommit();
// Commit all the side-effects within a tree. We'll do this in two passes.
// The first pass performs all the host insertions, updates, deletions and
// ref unmounts.
nextEffect = firstEffect;
while (nextEffect !== null) {
try {
commitAllHostEffects(finishedWork);
} catch (error) {
invariant(
nextEffect !== null,
'Should have next effect. This error is likely caused by a bug ' +
'in React. Please file an issue.'
);
captureError(nextEffect, error);
// Clean-up
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}
resetAfterCommit(commitInfo);
// The work-in-progress tree is now the current tree. This must come after
// the first pass of the commit phase, so that the previous tree is still
// current during componentWillUnmount, but before the second pass, so that
// the finished work is current during componentDidMount/Update.
root.current = finishedWork;
// In the second pass we'll perform all life-cycles and ref callbacks.
// Life-cycles happen as a separate pass so that all placements, updates,
// and deletions in the entire tree have already been invoked.
// This pass also triggers any renderer-specific initial effects.
nextEffect = firstEffect;
while (nextEffect !== null) {
try {
commitAllLifeCycles(finishedWork, nextEffect);
} catch (error) {
invariant(
nextEffect !== null,
'Should have next effect. This error is likely caused by a bug ' +
'in React. Please file an issue.'
);
captureError(nextEffect, error);
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}
isCommitting = false;
if (typeof onCommitRoot === 'function') {
onCommitRoot(finishedWork.stateNode);
}
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
}
// If we caught any errors during this commit, schedule their boundaries
// to update.
if (commitPhaseBoundaries) {
commitPhaseBoundaries.forEach(scheduleErrorRecovery);
commitPhaseBoundaries = null;
}
priorityContext = previousPriorityContext;
}
function resetWorkPriority(workInProgress : Fiber) {
let newPriority = NoWork;
// Check for pending update priority. This is usually null so it shouldn't
// be a perf issue.
const queue = workInProgress.updateQueue;
if (queue !== null) {
newPriority = getPendingPriority(queue);
}
// TODO: Coroutines need to visit stateNode
// progressedChild is going to be the child set with the highest priority.
// Either it is the same as child, or it just bailed out because it choose
// not to do the work.
let child = workInProgress.progressedChild;
while (child !== null) {
// Ensure that remaining work priority bubbles up.
if (child.pendingWorkPriority !== NoWork &&
(newPriority === NoWork ||
newPriority > child.pendingWorkPriority)) {
newPriority = child.pendingWorkPriority;
}
child = child.sibling;
}
workInProgress.pendingWorkPriority = newPriority;
}
function completeUnitOfWork(workInProgress : Fiber) : Fiber | null {
while (true) {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
const next = completeWork(current, workInProgress);
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
resetWorkPriority(workInProgress);
if (next !== null) {
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
}
// If completing this work spawned new work, do that next. We'll come
// back here again.
return next;
}
if (returnFiber !== null) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}
// If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if
// needed, by doing multiple passes over the effect list. We don't want
// to schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.
if (workInProgress.effectTag !== NoEffect) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
}
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root. Unless we're current performing deferred
// work, we should commit the completed work immediately. If we are
// performing deferred work, returning null indicates to the caller
// that we just completed the root so they can handle that case correctly.
if (nextPriorityLevel < HighPriority) {
// Otherwise, we should commit immediately.
commitAllWork(workInProgress);
} else {
pendingCommit = workInProgress;
}
return null;
}
}
}
function performUnitOfWork(workInProgress : Fiber) : Fiber | null {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
let next = beginWork(current, workInProgress, nextPriorityLevel);
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress);
}
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
if (__DEV__) {
ReactDebugCurrentFiber.current = null;
}
return next;
}
function performFailedUnitOfWork(workInProgress : Fiber) : Fiber | null {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress);
}
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
if (__DEV__) {
ReactDebugCurrentFiber.current = null;
}
return next;
}
function performDeferredWork(deadline) {
// We pass the lowest deferred priority here because it acts as a minimum.
// Higher priorities will also be performed.
isDeferredCallbackScheduled = false;
performWork(OffscreenPriority, deadline);
}
function performAnimationWork() {
isAnimationCallbackScheduled = false;
performWork(AnimationPriority, null);
}
function clearErrors() {
if (nextUnitOfWork === null) {
nextUnitOfWork = findNextUnitOfWork();
}
// Keep performing work until there are no more errors
while (capturedErrors !== null && capturedErrors.size &&
nextUnitOfWork !== null &&
nextPriorityLevel !== NoWork &&
nextPriorityLevel <= TaskPriority) {
if (hasCapturedError(nextUnitOfWork)) {
// Use a forked version of performUnitOfWork
nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
} else {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork === null) {
// If performUnitOfWork returns null, that means we just committed
// a root. Normally we'd need to clear any errors that were scheduled
// during the commit phase. But we're already clearing errors, so
// we can continue.
nextUnitOfWork = findNextUnitOfWork();
}
}
}
function workLoop(priorityLevel, deadline : Deadline | null, deadlineHasExpired : boolean) : boolean {
// Clear any errors.
clearErrors();
if (nextUnitOfWork === null) {
nextUnitOfWork = findNextUnitOfWork();
}
let hostRootTimeMarker;
if (
ReactFeatureFlags.logTopLevelRenders &&
nextUnitOfWork !== null &&
nextUnitOfWork.tag === HostRoot &&
nextUnitOfWork.child !== null
) {
const componentName = getComponentName(nextUnitOfWork.child) || '';
hostRootTimeMarker = 'React update: ' + componentName;
console.time(hostRootTimeMarker);
}
// If there's a deadline, and we're not performing Task work, perform work
// using this loop that checks the deadline on every iteration.
if (deadline !== null && priorityLevel > TaskPriority) {
// The deferred work loop will run until there's no time left in
// the current frame.
while (nextUnitOfWork !== null && !deadlineHasExpired) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// In a deferred work batch, iff nextUnitOfWork returns null, we just
// completed a root and a pendingCommit exists. Logically, we could
// omit either of the checks in the following condition, but we need
// both to satisfy Flow.
if (nextUnitOfWork === null && pendingCommit !== null) {
// If we have time, we should commit the work now.
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
commitAllWork(pendingCommit);
nextUnitOfWork = findNextUnitOfWork();
// Clear any errors that were scheduled during the commit phase.
clearErrors();
} else {
deadlineHasExpired = true;
}
// Otherwise the root will committed in the next frame.
}
} else {
deadlineHasExpired = true;
}
}
} else {
// If there's no deadline, or if we're performing Task work, use this loop
// that doesn't check how much time is remaining. It will keep running
// until we run out of work at this priority level.
while (nextUnitOfWork !== null &&
nextPriorityLevel !== NoWork &&
nextPriorityLevel <= priorityLevel) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
if (nextUnitOfWork === null) {
nextUnitOfWork = findNextUnitOfWork();
// performUnitOfWork returned null, which means we just committed a
// root. Clear any errors that were scheduled during the commit phase.
clearErrors();
}
}
}
if (hostRootTimeMarker) {
console.timeEnd(hostRootTimeMarker);
}
return deadlineHasExpired;
}
function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
invariant(
!isPerformingWork,
'performWork was called recursively. This error is likely caused ' +
'by a bug in React. Please file an issue.'
);
isPerformingWork = true;
const isPerformingDeferredWork = Boolean(deadline);
let deadlineHasExpired = false;
// This outer loop exists so that we can restart the work loop after
// catching an error. It also lets us flush Task work at the end of a
// deferred batch.
while (priorityLevel !== NoWork && !fatalError) {
invariant(
deadline !== null || (priorityLevel < HighPriority),
'Cannot perform deferred work without a deadline. This error is ' +
'likely caused by a bug in React. Please file an issue.'
);
// Before starting any work, check to see if there are any pending
// commits from the previous frame.
if (pendingCommit !== null && !deadlineHasExpired) {
commitAllWork(pendingCommit);
}
// Nothing in performWork should be allowed to throw. All unsafe
// operations must happen within workLoop, which is extracted to a
// separate function so that it can be optimized by the JS engine.
try {
priorityContextBeforeReconciliation = priorityContext;
priorityContext = nextPriorityLevel;
deadlineHasExpired = workLoop(priorityLevel, deadline, deadlineHasExpired);
} catch (error) {
// We caught an error during either the begin or complete phases.
const failedWork = nextUnitOfWork;
if (failedWork !== null) {
// Reset the priority context to its value before reconciliation.
priorityContext = priorityContextBeforeReconciliation;
// "Capture" the error by finding the nearest boundary. If there is no
// error boundary, the nearest host container acts as one. If
// captureError returns null, the error was intentionally ignored.
const maybeBoundary = captureError(failedWork, error);
if (maybeBoundary !== null) {
const boundary = maybeBoundary;
// Complete the boundary as if it rendered null. This will unmount
// the failed tree.
beginFailedWork(boundary.alternate, boundary, priorityLevel);
// The next unit of work is now the boundary that captured the error.
// Conceptually, we're unwinding the stack. We need to unwind the
// context stack, too, from the failed work to the boundary that
// captured the error.
// TODO: If we set the memoized props in beginWork instead of
// completeWork, rather than unwind the stack, we can just restart
// from the root. Can't do that until then because without memoized
// props, the nodes higher up in the tree will rerender unnecessarily.
unwindContexts(failedWork, boundary);
nextUnitOfWork = completeUnitOfWork(boundary);
}
// Continue performing work
continue;
} else if (fatalError === null) {
// There is no current unit of work. This is a worst-case scenario
// and should only be possible if there's a bug in the renderer, e.g.
// inside resetAfterCommit.
fatalError = error;
}
} finally {
priorityContext = priorityContextBeforeReconciliation;
}
// Stop performing work
priorityLevel = NoWork;
// If have we more work, and we're in a deferred batch, check to see
// if the deadline has expired.
if (nextPriorityLevel !== NoWork && isPerformingDeferredWork && !deadlineHasExpired) {
// We have more time to do work.
priorityLevel = nextPriorityLevel;
continue;
}
// There might be work left. Depending on the priority, we should
// either perform it now or schedule a callback to perform it later.
switch (nextPriorityLevel) {
case SynchronousPriority:
case TaskPriority:
// Perform work immediately by switching the priority level
// and continuing the loop.
priorityLevel = nextPriorityLevel;
break;
case AnimationPriority:
scheduleAnimationCallback(performAnimationWork);
// Even though the next unit of work has animation priority, there
// may still be deferred work left over as well. I think this is
// only important for unit tests. In a real app, a deferred callback
// would be scheduled during the next animation frame.
scheduleDeferredCallback(performDeferredWork);
break;
case HighPriority:
case LowPriority:
case OffscreenPriority:
scheduleDeferredCallback(performDeferredWork);
break;
}
}
const errorToThrow = fatalError || firstUncaughtError;
// We're done performing work. Time to clean up.
isPerformingWork = false;
fatalError = null;
firstUncaughtError = null;
capturedErrors = null;
failedBoundaries = null;
// It's safe to throw any unhandled errors.
if (errorToThrow !== null) {
throw errorToThrow;
}
}
// Returns the boundary that captured the error, or null if the error is ignored
function captureError(failedWork : Fiber, error : Error) : Fiber | null {
// It is no longer valid because we exited the user code.
ReactCurrentOwner.current = null;
if (__DEV__) {
ReactDebugCurrentFiber.current = null;
}
// It is no longer valid because this unit of work failed.
nextUnitOfWork = null;
// Search for the nearest error boundary.
let boundary : Fiber | null = null;
// Passed to logCapturedError()
let errorBoundaryFound : boolean = false;
let willRetry : boolean = false;
let errorBoundaryName : string | null = null;
// Host containers are a special case. If the failed work itself is a host
// container, then it acts as its own boundary. In all other cases, we
// ignore the work itself and only search through the parents.
if (failedWork.tag === HostRoot) {
boundary = failedWork;
if (isFailedBoundary(failedWork)) {
// If this root already failed, there must have been an error when
// attempting to unmount it. This is a worst-case scenario and
// should only be possible if there's a bug in the renderer.
fatalError = error;
}
} else {
let node = failedWork.return;
while (node !== null && boundary === null) {
if (node.tag === ClassComponent) {
const instance = node.stateNode;
if (typeof instance.unstable_handleError === 'function') {
errorBoundaryFound = true;
errorBoundaryName = getComponentName(node);
// Found an error boundary!
boundary = node;
willRetry = true;
}
} else if (node.tag === HostRoot) {
// Treat the root like a no-op error boundary.
boundary = node;
}
if (isFailedBoundary(node)) {
// This boundary is already in a failed state.
// If we're currently unmounting, that means this error was
// thrown while unmounting a failed subtree. We should ignore
// the error.
if (isUnmounting) {
return null;
}
// If we're in the commit phase, we should check to see if
// this boundary already captured an error during this commit.
// This case exists because multiple errors can be thrown during
// a single commit without interruption.
if (commitPhaseBoundaries !== null && (
commitPhaseBoundaries.has(node) ||
(node.alternate !== null && commitPhaseBoundaries.has(node.alternate))
)) {
// If so, we should ignore this error.
return null;
}
// The error should propagate to the next boundary -— we keep looking.
boundary = null;
willRetry = false;
}
node = node.return;
}
}
if (boundary !== null) {
// Add to the collection of failed boundaries. This lets us know that
// subsequent errors in this subtree should propagate to the next boundary.
if (failedBoundaries === null) {
failedBoundaries = new Set();
}
failedBoundaries.add(boundary);
// This method is unsafe outside of the begin and complete phases.
// We might be in the commit phase when an error is captured.
// The risk is that the return path from this Fiber may not be accurate.
// That risk is acceptable given the benefit of providing users more context.
const componentStack = getStackAddendumByWorkInProgressFiber(failedWork);
const componentName = getComponentName(failedWork);
// Add to the collection of captured errors. This is stored as a global
// map of errors and their component stack location keyed by the boundaries
// that capture them. We mostly use this Map as a Set; it's a Map only to
// avoid adding a field to Fiber to store the error.
if (capturedErrors === null) {
capturedErrors = new Map();
}
capturedErrors.set(boundary, {
componentName,
componentStack,
error,
errorBoundary: errorBoundaryFound ? boundary.stateNode : null,
errorBoundaryFound,
errorBoundaryName,
willRetry,
});
// If we're in the commit phase, defer scheduling an update on the
// boundary until after the commit is complete
if (isCommitting) {
if (commitPhaseBoundaries === null) {
commitPhaseBoundaries = new Set();
}
commitPhaseBoundaries.add(boundary);
} else {
// Otherwise, schedule an update now.
scheduleErrorRecovery(boundary);
}
return boundary;
} else if (firstUncaughtError === null) {
// If no boundary is found, we'll need to throw the error
firstUncaughtError = error;
}
return null;
}
function hasCapturedError(fiber : Fiber) : boolean {
// TODO: capturedErrors should store the boundary instance, to avoid needing
// to check the alternate.
return Boolean(
capturedErrors !== null &&
(capturedErrors.has(fiber) || (fiber.alternate !== null && capturedErrors.has(fiber.alternate)))
);
}
function isFailedBoundary(fiber : Fiber) : boolean {
// TODO: failedBoundaries should store the boundary instance, to avoid
// needing to check the alternate.
return Boolean(
failedBoundaries !== null &&
(failedBoundaries.has(fiber) || (fiber.alternate !== null && failedBoundaries.has(fiber.alternate)))
);
}
function commitErrorHandling(effectfulFiber : Fiber) {
let capturedError;
if (capturedErrors !== null) {
capturedError = capturedErrors.get(effectfulFiber);
capturedErrors.delete(effectfulFiber);
if (capturedError == null) {
if (effectfulFiber.alternate !== null) {
effectfulFiber = effectfulFiber.alternate;
capturedError = capturedErrors.get(effectfulFiber);
capturedErrors.delete(effectfulFiber);
}
}
}
invariant(
capturedError != null,
'No error for given unit of work. This error is likely caused by a ' +
'bug in React. Please file an issue.'
);
const error = capturedError.error;
try {
logCapturedError(capturedError);
} catch (e) {
// Prevent cycle if logCapturedError() throws.
// A cycle may still occur if logCapturedError renders a component that throws.
console.error(e);
}
switch (effectfulFiber.tag) {
case ClassComponent:
const instance = effectfulFiber.stateNode;
// Allow the boundary to handle the error, usually by scheduling
// an update to itself
instance.unstable_handleError(error);
return;
case HostRoot:
if (firstUncaughtError === null) {
// If this is the host container, we treat it as a no-op error
// boundary. We'll throw the first uncaught error once it's safe to
// do so, at the end of the batch.
firstUncaughtError = error;
}
return;
default:
invariant(
false,
'Invalid type of work. This error is likely caused by a bug in ' +
'React. Please file an issue.'
);
}
}
function unwindContexts(from : Fiber, to: Fiber) {
let node = from;
while (node !== null && (node !== to) && (node.alternate !== to)) {
switch (node.tag) {
case ClassComponent:
popContextProvider(node);
break;
case HostComponent:
popHostContext(node);
break;
case HostRoot:
popHostContainer(node);
break;
case HostPortal:
popHostContainer(node);
break;
}
node = node.return;
}
}
function scheduleRoot(root : FiberRoot, priorityLevel : PriorityLevel) {
if (priorityLevel === NoWork) {
return;
}
if (!root.isScheduled) {
root.isScheduled = true;
if (lastScheduledRoot) {
// Schedule ourselves to the end.
lastScheduledRoot.nextScheduledRoot = root;
lastScheduledRoot = root;
} else {
// We're the only work scheduled.
nextScheduledRoot = root;
lastScheduledRoot = root;
}
}
}
function scheduleUpdate(fiber : Fiber, priorityLevel : PriorityLevel) {
if (priorityLevel <= nextPriorityLevel) {
// We must reset the current unit of work pointer so that we restart the
// search from the root during the next tick, in case there is now higher
// priority work somewhere earlier than before.
nextUnitOfWork = null;
}
let node = fiber;
let shouldContinue = true;
while (node !== null && shouldContinue) {
// Walk the parent path to the root and update each node's priority. Once
// we reach a node whose priority matches (and whose alternate's priority
// matches) we can exit safely knowing that the rest of the path is correct.
shouldContinue = false;
if (node.pendingWorkPriority === NoWork ||
node.pendingWorkPriority > priorityLevel) {
// Priority did not match. Update and keep going.
shouldContinue = true;
node.pendingWorkPriority = priorityLevel;
}
if (node.alternate !== null) {
if (node.alternate.pendingWorkPriority === NoWork ||
node.alternate.pendingWorkPriority > priorityLevel) {
// Priority did not match. Update and keep going.
shouldContinue = true;
node.alternate.pendingWorkPriority = priorityLevel;
}
}
if (node.return === null) {
if (node.tag === HostRoot) {
const root : FiberRoot = (node.stateNode : any);
scheduleRoot(root, priorityLevel);
// Depending on the priority level, either perform work now or
// schedule a callback to perform work later.
switch (priorityLevel) {
case SynchronousPriority:
performWork(SynchronousPriority, null);
return;
case TaskPriority:
// TODO: If we're not already performing work, schedule a
// deferred callback.
return;
case AnimationPriority:
scheduleAnimationCallback(performAnimationWork);
return;
case HighPriority:
case LowPriority:
case OffscreenPriority:
scheduleDeferredCallback(performDeferredWork);
return;
}
} else {
if (__DEV__) {
if (fiber.tag === ClassComponent) {
warnAboutUpdateOnUnmounted(fiber.stateNode);
}
}
return;
}
}
node = node.return;
}
}
function getPriorityContext() : PriorityLevel {
// If we're in a batch, or if we're already performing work, downgrade sync
// priority to task priority
if (priorityContext === SynchronousPriority && (isPerformingWork || isBatchingUpdates)) {
return TaskPriority;
}
return priorityContext;
}
function scheduleErrorRecovery(fiber : Fiber) {
scheduleUpdate(fiber, TaskPriority);
}
function performWithPriority(priorityLevel : PriorityLevel, fn : Function) {
const previousPriorityContext = priorityContext;
priorityContext = priorityLevel;
try {
fn();
} finally {
priorityContext = previousPriorityContext;
}
}
function batchedUpdates<A, R>(fn : (a: A) => R, a : A) : R {
const previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
return fn(a);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
// If we're not already inside a batch, we need to flush any task work
// that was created by the user-provided function.
if (!isPerformingWork && !isBatchingUpdates) {
performWork(TaskPriority, null);
}
}
}
function unbatchedUpdates<A>(fn : () => A) : A {
const previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = false;
try {
return fn();
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
}
}
function syncUpdates<A>(fn : () => A) : A {
const previousPriorityContext = priorityContext;
priorityContext = SynchronousPriority;
try {
return fn();
} finally {
priorityContext = previousPriorityContext;
}
}
function deferredUpdates<A>(fn : () => A) : A {
const previousPriorityContext = priorityContext;
priorityContext = LowPriority;
try {
return fn();
} finally {
priorityContext = previousPriorityContext;
}
}
return {
scheduleUpdate: scheduleUpdate,
getPriorityContext: getPriorityContext,
performWithPriority: performWithPriority,
batchedUpdates: batchedUpdates,
unbatchedUpdates: unbatchedUpdates,
syncUpdates: syncUpdates,
deferredUpdates: deferredUpdates,
};
};