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; } }
function performWork( minPriorityLevel: 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; let hasRemainingAsyncWork = 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 (fatalError === null) { // Before starting any work, check to see if there are any pending // commits from the previous frame. // TODO: Only commit asynchronous priority at beginning or end of a frame. // Task work can be committed whenever. if (pendingCommit !== null && !deadlineHasExpired) { // Safe to call this outside the work loop because the commit phase has // its own try-catch. 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, minPriorityLevel, deadline, ); } else { try { workLoop(minPriorityLevel, 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, nextPriorityLevel); // 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; } } else { // 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: // We have remaining synchronous or task work. Keep performing it, // regardless of whether we're inside a callback. if (nextPriorityLevel <= minPriorityLevel) { continue; } break; case HighPriority: case LowPriority: case OffscreenPriority: // We have remaining async work. if (deadline === null) { // We're not inside a callback. Exit and perform the work during // the next callback. hasRemainingAsyncWork = true; } else { // We are inside a callback. if ( !deadlineHasExpired && nextPriorityLevel <= minPriorityLevel ) { // We still have time. Keep working. continue; } // We've run out of time. Exit. hasRemainingAsyncWork = true; } break; case NoWork: // No work left. We can exit. break; default: invariant(false, 'Switch statement should be exhuastive.'); } // Exit the loop. break; } } // If we're inside a callback, set this to false, since we just flushed it. if (deadline !== null) { isCallbackScheduled = false; } // If there's remaining async work, make sure we schedule another callback. if (hasRemainingAsyncWork && !isCallbackScheduled) { scheduleDeferredCallback(performDeferredWork); isCallbackScheduled = true; } const errorToThrow = fatalError !== null ? 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; } }