react-native-macos
Version:
A framework for building native macOS apps using React
1,442 lines (1,293 loc) • 48.6 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,
};
export type HandleErrorInfo = {
componentStack: string,
};
var {popContextProvider} = require('ReactFiberContext');
const {reset} = require('ReactFiberStack');
var {
getStackAddendumByWorkInProgressFiber,
} = require('ReactFiberComponentTreeHook');
var {logCapturedError} = require('ReactFiberErrorLogger');
var {invokeGuardedCallback} = require('ReactErrorUtils');
var ReactFiberBeginWork = require('ReactFiberBeginWork');
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
var ReactFiberCommitWork = require('ReactFiberCommitWork');
var ReactFiberHostContext = require('ReactFiberHostContext');
var ReactFeatureFlags = require('ReactFeatureFlags');
var {ReactCurrentOwner} = require('ReactGlobalSharedState');
var getComponentName = require('getComponentName');
var {cloneFiber} = require('ReactFiber');
var {onCommitRoot} = require('ReactFiberDevToolsHook');
var {
NoWork,
SynchronousPriority,
TaskPriority,
AnimationPriority,
HighPriority,
LowPriority,
OffscreenPriority,
} = require('ReactPriorityLevel');
var {AsyncUpdates} = require('ReactTypeOfInternalContext');
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 {
recordEffect,
recordScheduleUpdate,
startWorkTimer,
stopWorkTimer,
startWorkLoopTimer,
stopWorkLoopTimer,
startCommitTimer,
stopCommitTimer,
startCommitHostEffectsTimer,
stopCommitHostEffectsTimer,
startCommitLifeCyclesTimer,
stopCommitLifeCyclesTimer,
} = require('ReactDebugFiberPerf');
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 warnAboutInvalidUpdates = function(instance: ReactClass<any>) {
switch (ReactDebugCurrentFiber.phase) {
case 'getChildContext':
warning(
false,
'setState(...): Cannot call setState() inside getChildContext()',
);
break;
case 'render':
warning(
false,
'Cannot update during an existing state transition (such as within ' +
"`render` or another component's constructor). Render methods should " +
'be a pure function of props and state; constructor side-effects are ' +
'an anti-pattern, but can be moved to `componentWillMount`.',
);
break;
}
};
}
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,
commitAttachRef,
commitDetachRef,
} = ReactFiberCommitWork(config, captureError);
const {
scheduleAnimationCallback: hostScheduleAnimationCallback,
scheduleDeferredCallback: hostScheduleDeferredCallback,
useSyncScheduling,
prepareForCommit,
resetAfterCommit,
} = config;
// The priority level to use when scheduling an update. We use NoWork to
// represent the default priority.
// TODO: Should we change this to an array instead of using the call stack?
// Might be less confusing.
let priorityContext: PriorityLevel = NoWork;
// 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 the current deadline has expired.
let deadlineHasExpired: 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;
recordEffect();
}
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
config.resetTextContent(nextEffect.stateNode);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 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 = 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 effectTag = nextEffect.effectTag;
// Use Task priority for lifecycle updates
if (effectTag & (Update | Callback)) {
if (__DEV__) {
recordEffect();
}
const current = nextEffect.alternate;
commitLifeCycles(current, nextEffect);
}
if (effectTag & Ref) {
if (__DEV__) {
recordEffect();
}
commitAttachRef(nextEffect);
}
if (effectTag & Err) {
if (__DEV__) {
recordEffect();
}
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;
if (__DEV__) {
startCommitTimer();
}
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;
if (__DEV__) {
startCommitHostEffectsTimer();
}
while (nextEffect !== null) {
let error = null;
if (__DEV__) {
error = invokeGuardedCallback(
null,
commitAllHostEffects,
null,
finishedWork,
);
} else {
try {
commitAllHostEffects(finishedWork);
} catch (e) {
error = e;
}
}
if (error !== null) {
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;
}
}
}
if (__DEV__) {
stopCommitHostEffectsTimer();
}
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;
if (__DEV__) {
startCommitLifeCyclesTimer();
}
while (nextEffect !== null) {
let error = null;
if (__DEV__) {
error = invokeGuardedCallback(
null,
commitAllLifeCycles,
null,
finishedWork,
);
} else {
try {
commitAllLifeCycles(finishedWork);
} catch (e) {
error = e;
}
}
if (error !== null) {
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 (__DEV__) {
stopCommitLifeCyclesTimer();
stopCommitTimer();
}
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;
const tag = workInProgress.tag;
if (
queue !== null &&
// TODO: Revisit once updateQueue is typed properly to distinguish between
// update payloads for host components and update queues for composites
(tag === ClassComponent || tag === HostRoot)
) {
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__) {
stopWorkTimer(workInProgress);
}
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__) {
stopWorkTimer(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;
}
}
// Without this explicit null return Flow complains of invalid return type
// TODO Remove the above while(true) loop
// eslint-disable-next-line no-unreachable
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.
if (__DEV__) {
startWorkTimer(workInProgress);
}
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.
if (__DEV__) {
startWorkTimer(workInProgress);
}
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) {
// 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);
}
}
function performWork(
priorityLevel: PriorityLevel,
deadline: Deadline | null,
) {
if (__DEV__) {
startWorkLoopTimer();
}
invariant(
!isPerformingWork,
'performWork was called recursively. This error is likely caused ' +
'by a bug in React. Please file an issue.',
);
isPerformingWork = true;
const isPerformingDeferredWork = !!deadline;
// 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.
priorityContextBeforeReconciliation = priorityContext;
let error = null;
if (__DEV__) {
error = invokeGuardedCallback(
null,
workLoop,
null,
priorityLevel,
deadline,
);
} else {
try {
workLoop(priorityLevel, deadline);
} catch (e) {
error = e;
}
}
// Reset the priority context to its value before reconcilation.
priorityContext = priorityContextBeforeReconciliation;
if (error !== null) {
// We caught an error during either the begin or complete phases.
const failedWork = nextUnitOfWork;
if (failedWork !== null) {
// "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;
}
}
// 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;
deadlineHasExpired = false;
fatalError = null;
firstUncaughtError = null;
capturedErrors = null;
failedBoundaries = null;
if (__DEV__) {
stopWorkLoopTimer();
}
// 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;
ReactDebugCurrentFiber.phase = 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 (
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 (
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;
const info: HandleErrorInfo = {
componentStack: capturedError.componentStack,
};
// Allow the boundary to handle the error, usually by scheduling
// an update to itself
instance.unstable_handleError(error, info);
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;
}
if (__DEV__) {
stopWorkTimer(node);
}
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 (__DEV__) {
recordScheduleUpdate();
}
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;
}
if (__DEV__) {
if (fiber.tag === ClassComponent) {
const instance = fiber.stateNode;
warnAboutInvalidUpdates(instance);
}
}
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(
fiber: Fiber,
forceAsync: boolean,
): PriorityLevel {
let priorityLevel = priorityContext;
if (priorityLevel === NoWork) {
if (
!useSyncScheduling ||
fiber.internalContextTag & AsyncUpdates ||
forceAsync
) {
priorityLevel = LowPriority;
} else {
priorityLevel = SynchronousPriority;
}
}
// If we're in a batch, or if we're already performing work, downgrade sync
// priority to task priority
if (
priorityLevel === SynchronousPriority &&
(isPerformingWork || isBatchingUpdates)
) {
return TaskPriority;
}
return priorityLevel;
}
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,
};
};