Example #1
0
  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;
    }
  }