React16源码: React中的renderRoot的错误处理的源码实现

renderRoot的错误处理


1 )概述

  • completeWork这个方法之后, 再次回到 renderRoot 里面
  • renderRoot 里面执行了 workLoop, 之后,对 workLoop 使用了try catch
  • 如果在里面有任何一个节点在更新的过程当中 throw Error 都会被catch到
  • catch到之后就是错误处理
    • 给报错节点增加 incomplete 副作用
      • incomplete 的副作用在 completeUnitOfWork 的时候,用来进行判断
      • 是要调用 completeWork,还是调用 unwindWork
    • 需要给父链上具有 error boundary 的节点增加副作用
      • 让它去收集错误以及进行一定的处理
    • 还需要创建错误相关的更新

2 )源码

定位到 packages/react-reconciler/src/ReactFiberScheduler.js#L1293

renderRoot 里面的 do while 循环中

do {
    try {
      workLoop(isYieldy);
    // 在catch里面得到了一个 thrownValue, 这是一个error
    } catch (thrownValue) {
      resetContextDependences();
      resetHooks();

      // Reset in case completion throws.
      // This is only used in DEV and when replaying is on.
      let mayReplay;
      if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
        mayReplay = mayReplayFailedUnitOfWork;
        mayReplayFailedUnitOfWork = true;
      }

      // 如果 nextUnitOfWork 等于 null,这是一个不属于正常流程里面的一个情况
      // 因为 nextUnitOfWork 是我们在更新一个节点之前,它是有值的更新
      // 更新节点之后,它会被赋成一个新的值
      // 一般来说它不应该是会存在 nextUnitOfWork 是 null 的一个情况
      // 即便 throw error 了,上一个 nextUnitOfWork 也没有主动去把它消除
      if (nextUnitOfWork === null) {
        // This is a fatal error.
        // 为 null 这种情况,被认为是一个致命的错误
        didFatal = true;
        // 调用 onUncaughtError,无法处理的错误被抛出了
        // 这个时候,react是会直接中断渲染染流程
        onUncaughtError(thrownValue);
      } else {
        // 非上述致命错误
        if (enableProfilerTimer && nextUnitOfWork.mode & ProfileMode) {
          // Record the time spent rendering before an error was thrown.
          // This avoids inaccurate Profiler durations in the case of a suspended render.
          stopProfilerTimerIfRunningAndRecordDelta(nextUnitOfWork, true);
        }

        if (__DEV__) {
          // Reset global debug state
          // We assume this is defined in DEV
          (resetCurrentlyProcessingQueue: any)();
        }

        if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
          if (mayReplay) {
            const failedUnitOfWork: Fiber = nextUnitOfWork;
            replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
          }
        }

        // TODO: we already know this isn't true in some cases.
        // At least this shows a nicer error message until we figure out the cause.
        // https://github.com/facebook/react/issues/12449#issuecomment-386727431
        invariant(
          nextUnitOfWork !== null,
          'Failed to replay rendering after an error. This ' +
            'is likely caused by a bug in React. Please file an issue ' +
            'with a reproducing case to help us find it.',
        );

        const sourceFiber: Fiber = nextUnitOfWork;
        let returnFiber = sourceFiber.return;
        
        // 如果 returnFiber 等于 null, 说明这个错误是出现在更新 RootFiber 的过程当中
        // 那么 RootFiber 它是一个非常固定的节点,就是说它没有用户代码去进行一个参与的
        // 如果这个它出现了一个错误,也是一个致命的错误,是一个react源码级的一个错误,同上一样处理
        if (returnFiber === null) {
          // This is the root. The root could capture its own errors. However,
          // we don't know if it errors before or after we pushed the host
          // context. This information is needed to avoid a stack mismatch.
          // Because we're not sure, treat this as a fatal error. We could track
          // which phase it fails in, but doesn't seem worth it. At least
          // for now.
          didFatal = true;
          onUncaughtError(thrownValue);
        } else {
          // 这里是常规处理错误
          throwException(
            root,
            returnFiber,
            sourceFiber,
            thrownValue,
            nextRenderExpirationTime,
          );
          // 立刻完成当前节点,因为出错了,子树渲染没有意义了
          nextUnitOfWork = completeUnitOfWork(sourceFiber);
          continue;
        }
      }
    }
    break;
  } while (true);
  • 进入 onUncaughtError

    function onUncaughtError(error: mixed) {
      invariant(
        nextFlushedRoot !== null,
        'Should be working on a root. This error is likely caused by a bug in ' +
          'React. Please file an issue.',
      );
      // Unschedule this root so we don't work on it again until there's
      // another update.
      // 这时候直接把 nextFlushedRoot 设置成 NoWork,剩下的任务都不再执行了
      nextFlushedRoot.expirationTime = NoWork;
      // 处理全局变量
      if (!hasUnhandledError) {
        hasUnhandledError = true;
        unhandledError = error;
      }
    }
    
  • 进入 throwException 这个是常规处理错误的处理器

    function throwException(
      root: FiberRoot,
      returnFiber: Fiber,
      sourceFiber: Fiber,
      value: mixed,
      renderExpirationTime: ExpirationTime,
    ) {
      // 增加 Incomplete 这个 SideEffect
      // 这也是在 completeUnitOfWork 当中,去判断节点,是否有 Incomplete 的来源
      // 只有在这个节点,throw了一个异常之后,它才会被赋值 SideEffect
      // The source fiber did not complete.
      sourceFiber.effectTag |= Incomplete;
      // Its effect list is no longer valid.
      // 对 firstEffect 和 lastEffect 置 null
      // 因为它已经抛出一个异常了,子节点不会再进行渲染,不会有effect的一个链
      sourceFiber.firstEffect = sourceFiber.lastEffect = null;
    
      // 这种就匹配 Promise对象,或者 thenable 对象
      // 这个其实就对应 Suspense 相关的处理,通过 throw 一个 thenable 的对象
      // 可以让这个组件变成一个挂起的状态,等到这个 Promise 被 resolve之后,再次进入一个正常的渲染周期
      // 这部分都是跟 Suspense 相关的代码,先跳过
      if (
        value !== null &&
        typeof value === 'object' &&
        typeof value.then === 'function'
      ) {
        // This is a thenable.
        const thenable: Thenable = (value: any);
    
        // Find the earliest timeout threshold of all the placeholders in the
        // ancestor path. We could avoid this traversal by storing the thresholds on
        // the stack, but we choose not to because we only hit this path if we're
        // IO-bound (i.e. if something suspends). Whereas the stack is used even in
        // the non-IO- bound case.
        let workInProgress = returnFiber;
        let earliestTimeoutMs = -1;
        let startTimeMs = -1;
        do {
          if (workInProgress.tag === SuspenseComponent) {
            const current = workInProgress.alternate;
            if (current !== null) {
              const currentState: SuspenseState | null = current.memoizedState;
              if (currentState !== null && currentState.didTimeout) {
                // Reached a boundary that already timed out. Do not search
                // any further.
                const timedOutAt = currentState.timedOutAt;
                startTimeMs = expirationTimeToMs(timedOutAt);
                // Do not search any further.
                break;
              }
            }
            let timeoutPropMs = workInProgress.pendingProps.maxDuration;
            if (typeof timeoutPropMs === 'number') {
              if (timeoutPropMs <= 0) {
                earliestTimeoutMs = 0;
              } else if (
                earliestTimeoutMs === -1 ||
                timeoutPropMs < earliestTimeoutMs
              ) {
                earliestTimeoutMs = timeoutPropMs;
              }
            }
          }
          workInProgress = workInProgress.return;
        } while (workInProgress !== null);
    
        // Schedule the nearest Suspense to re-render the timed out view.
        workInProgress = returnFiber;
        do {
          if (
            workInProgress.tag === SuspenseComponent &&
            shouldCaptureSuspense(workInProgress.alternate, workInProgress)
          ) {
            // Found the nearest boundary.
    
            // If the boundary is not in concurrent mode, we should not suspend, and
            // likewise, when the promise resolves, we should ping synchronously.
            const pingTime =
              (workInProgress.mode & ConcurrentMode) === NoEffect
                ? Sync
                : renderExpirationTime;
    
            // Attach a listener to the promise to "ping" the root and retry.
            let onResolveOrReject = retrySuspendedRoot.bind(
              null,
              root,
              workInProgress,
              sourceFiber,
              pingTime,
            );
            if (enableSchedulerTracing) {
              onResolveOrReject = Schedule_tracing_wrap(onResolveOrReject);
            }
            thenable.then(onResolveOrReject, onResolveOrReject);
    
            // If the boundary is outside of concurrent mode, we should *not*
            // suspend the commit. Pretend as if the suspended component rendered
            // null and keep rendering. In the commit phase, we'll schedule a
            // subsequent synchronous update to re-render the Suspense.
            //
            // Note: It doesn't matter whether the component that suspended was
            // inside a concurrent mode tree. If the Suspense is outside of it, we
            // should *not* suspend the commit.
            if ((workInProgress.mode & ConcurrentMode) === NoEffect) {
              workInProgress.effectTag |= CallbackEffect;
    
              // Unmount the source fiber's children
              const nextChildren = null;
              reconcileChildren(
                sourceFiber.alternate,
                sourceFiber,
                nextChildren,
                renderExpirationTime,
              );
              sourceFiber.effectTag &= ~Incomplete;
    
              if (sourceFiber.tag === ClassComponent) {
                // We're going to commit this fiber even though it didn't complete.
                // But we shouldn't call any lifecycle methods or callbacks. Remove
                // all lifecycle effect tags.
                sourceFiber.effectTag &= ~LifecycleEffectMask;
                const current = sourceFiber.alternate;
                if (current === null) {
                  // This is a new mount. Change the tag so it's not mistaken for a
                  // completed component. For example, we should not call
                  // componentWillUnmount if it is deleted.
                  sourceFiber.tag = IncompleteClassComponent;
                }
              }
    
              // Exit without suspending.
              return;
            }
    
            // Confirmed that the boundary is in a concurrent mode tree. Continue
            // with the normal suspend path.
    
            let absoluteTimeoutMs;
            if (earliestTimeoutMs === -1) {
              // If no explicit threshold is given, default to an abitrarily large
              // value. The actual size doesn't matter because the threshold for the
              // whole tree will be clamped to the expiration time.
              absoluteTimeoutMs = maxSigned31BitInt;
            } else {
              if (startTimeMs === -1) {
                // This suspend happened outside of any already timed-out
                // placeholders. We don't know exactly when the update was
                // scheduled, but we can infer an approximate start time from the
                // expiration time. First, find the earliest uncommitted expiration
                // time in the tree, including work that is suspended. Then subtract
                // the offset used to compute an async update's expiration time.
                // This will cause high priority (interactive) work to expire
                // earlier than necessary, but we can account for this by adjusting
                // for the Just Noticeable Difference.
                const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
                  root,
                  renderExpirationTime,
                );
                const earliestExpirationTimeMs = expirationTimeToMs(
                  earliestExpirationTime,
                );
                startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
              }
              absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;
            }
    
            // Mark the earliest timeout in the suspended fiber's ancestor path.
            // After completing the root, we'll take the largest of all the
            // suspended fiber's timeouts and use it to compute a timeout for the
            // whole tree.
            renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);
    
            workInProgress.effectTag |= ShouldCapture;
            workInProgress.expirationTime = renderExpirationTime;
            return;
          }
          // This boundary already captured during this render. Continue to the next
          // boundary.
          workInProgress = workInProgress.return;
        } while (workInProgress !== null);
        // No boundary was found. Fallthrough to error mode.
        value = new Error(
          'An update was suspended, but no placeholder UI was provided.',
        );
      }
    
      // We didn't find a boundary that could handle this type of exception. Start
      // over and traverse parent path again, this time treating the exception
      // as an error.
      // renderDidError 方法 就是设置全局变量 nextRenderDidError 为 true
      renderDidError();
      // 返回错误调用信息字符串
      value = createCapturedValue(value, sourceFiber);
      let workInProgress = returnFiber;
      // 根据tag匹配处理程序
      do {
        // 它其实就是往上去找它,要找到第一个可以处理错误的 class component 
        // 来进行一个错误的update的一个创建,并且让它入栈
        // 等后期在commit的时候可以进行一个调用
        // 如果都没有,那么它会到 HostRoot 上面来进行处理错误
        // 因为 HostRoot 它相当于是一个内置的错误处理的方式
        // 也会创建对应的update,然后进行一个入队列,然后后续进行一个调用的过程
        // 这就是一个 throw exception,对于错误处理的一个情况
        switch (workInProgress.tag) {
          case HostRoot: {
            const errorInfo = value;
            workInProgress.effectTag |= ShouldCapture;
            workInProgress.expirationTime = renderExpirationTime;
            // 这个 update 类似于 setState 创建的对象
            const update = createRootErrorUpdate(
              workInProgress,
              errorInfo,
              renderExpirationTime,
            );
            enqueueCapturedUpdate(workInProgress, update);
            return;
          }
          // 
          case ClassComponent:
            // Capture and retry
            const errorInfo = value;
            const ctor = workInProgress.type;
            const instance = workInProgress.stateNode;
            // 它要先去判断一下,它现在没有 DidCapture 这个 SideEffect
            // 并且它是有 getDerivedStateFromError 这么一个方法
            // 或者它是有 componentDidCatch 生命周期方法
            if (
              (workInProgress.effectTag & DidCapture) === NoEffect &&
              (typeof ctor.getDerivedStateFromError === 'function' ||
                (instance !== null &&
                  typeof instance.componentDidCatch === 'function' &&
                  !isAlreadyFailedLegacyErrorBoundary(instance)))
            ) {
              // 在这种情况下,我们就可以给它加上 ShouldCapture 这个 SideEffect
              // 并且呢设置它的 expirationTime 等于 renderExpirationTime
              // 因为我要去对这个组件, 在这个周期里面进行一个更新的过程
              // 然后他也要去创建一个update调用的是 createClassErrorUpdate
              workInProgress.effectTag |= ShouldCapture;
              workInProgress.expirationTime = renderExpirationTime;
              // Schedule the error boundary to re-render using updated state
              const update = createClassErrorUpdate(
                workInProgress,
                errorInfo,
                renderExpirationTime,
              );
              enqueueCapturedUpdate(workInProgress, update);
              return;
            }
            break;
          default:
            break;
        }
        workInProgress = workInProgress.return;
      } while (workInProgress !== null);
    }
    
    • 进入 renderDidError
      // packages/react-reconciler/src/ReactFiberScheduler.js
      function renderDidError() {
        nextRenderDidError = true;
      }
      
    • 进入 createCapturedValue
      // packages/react-reconciler/src/ReactCapturedValue.js
      export function createCapturedValue<T>(
        value: T,
        source: Fiber,
      ): CapturedValue<T> {
        // If the value is an error, call this function immediately after it is thrown
        // so the stack is accurate.
        return {
          value,
          source,
          stack: getStackByFiberInDevAndProd(source),
        };
      }
      
      • 进入 getStackByFiberInDevAndProd
        function describeFiber(fiber: Fiber): string {
          switch (fiber.tag) {
            case IndeterminateComponent:
            case LazyComponent:
            case FunctionComponent:
            case ClassComponent:
            case HostComponent:
            case Mode:
              const owner = fiber._debugOwner;
              const source = fiber._debugSource;
              const name = getComponentName(fiber.type);
              let ownerName = null;
              if (owner) {
                ownerName = getComponentName(owner.type);
              }
              return describeComponentFrame(name, source, ownerName);
            default:
              return '';
          }
        }
        
        // 类似于js里面的error对象,它会有一个stack的信息
        // 就是哪个文件或者哪个方法调用的时候,它出现了错误,并且附上文件对应的代码的行数之类的信息
        export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
          let info = '';
          let node = workInProgress;
          // 形成一个错误调用信息的过程
          do {
            info += describeFiber(node);
            node = node.return;
          } while (node);
          return info;
        }
        
      • 进入 createRootErrorUpdate
        function createRootErrorUpdate(
          fiber: Fiber,
          errorInfo: CapturedValue<mixed>,
          expirationTime: ExpirationTime,
        ): Update<mixed> {
          const update = createUpdate(expirationTime);
          // Unmount the root by rendering null.
          update.tag = CaptureUpdate;
          // Caution: React DevTools currently depends on this property
          // being called "element".
          update.payload = {element: null};
          const error = errorInfo.value;
          update.callback = () => {
            // 打印 error
            onUncaughtError(error);
            logError(fiber, errorInfo);
          };
          return update;
        }
        
      • 进入 enqueueCapturedUpdate
        // 没有则创建,有则克隆
        // 挂载 update
        export function enqueueCapturedUpdate<State>(
          workInProgress: Fiber,
          update: Update<State>,
        ) {
          // Captured updates go into a separate list, and only on the work-in-
          // progress queue.
          let workInProgressQueue = workInProgress.updateQueue;
          if (workInProgressQueue === null) {
            workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(
              workInProgress.memoizedState,
            );
          } else {
            // TODO: I put this here rather than createWorkInProgress so that we don't
            // clone the queue unnecessarily. There's probably a better way to
            // structure this.
            workInProgressQueue = ensureWorkInProgressQueueIsAClone(
              workInProgress,
              workInProgressQueue,
            );
          }
        
          // Append the update to the end of the list.
          if (workInProgressQueue.lastCapturedUpdate === null) {
            // This is the first render phase update
            workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;
          } else {
            workInProgressQueue.lastCapturedUpdate.next = update;
            workInProgressQueue.lastCapturedUpdate = update;
          }
        }
        
      • 进入 createClassErrorUpdate
        function createClassErrorUpdate(
          fiber: Fiber,
          errorInfo: CapturedValue<mixed>,
          expirationTime: ExpirationTime,
        ): Update<mixed> {
          // 创建 update
          const update = createUpdate(expirationTime);
          // 标记 tag
          update.tag = CaptureUpdate;
          const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
          // 存在 getDerivedStateFromError 则作为 payload 回调处理
          if (typeof getDerivedStateFromError === 'function') {
            const error = errorInfo.value;
            update.payload = () => {
              return getDerivedStateFromError(error);
            };
          }
        
          const inst = fiber.stateNode;
          if (inst !== null && typeof inst.componentDidCatch === 'function') {
        
            // 这个有组件错误被捕获之后,它会向上去找有能够处理捕获错误信息的这个class component 来处理
            // 如果都没有,它才会到 root 上面来进行一个处理
            // 它会根据像 getDerivedStateFromError 以及 componentDidCatch 这些生命周期方法来进行一个处理
            // 如果都没有这个指定,那么这个classcomponent 是没有一个错误处理的功能的
            // 如果有就会对应的进行这些操作来进行一个调用
            update.callback = function callback() {
              if (typeof getDerivedStateFromError !== 'function') {
                // To preserve the preexisting retry behavior of error boundaries,
                // we keep track of which ones already failed during this batch.
                // This gets reset before we yield back to the browser.
                // TODO: Warn in strict mode if getDerivedStateFromError is
                // not defined.
                markLegacyErrorBoundaryAsFailed(this);
              }
              const error = errorInfo.value;
              const stack = errorInfo.stack;
              // 输出 error
              logError(fiber, errorInfo);
              // 调用catch回调钩子 传入 stack
              this.componentDidCatch(error, {
                componentStack: stack !== null ? stack : '',
              });
              if (__DEV__) {
                if (typeof getDerivedStateFromError !== 'function') {
                  // If componentDidCatch is the only error boundary method defined,
                  // then it needs to call setState to recover from errors.
                  // If no state update is scheduled then the boundary will swallow the error.
                  warningWithoutStack(
                    fiber.expirationTime === Sync,
                    '%s: Error boundaries should implement getDerivedStateFromError(). ' +
                      'In that method, return a state update to display an error message or fallback UI.',
                    getComponentName(fiber.type) || 'Unknown',
                  );
                }
              }
            };
          }
          return update;
        }
        
  • 经过以上的处理,在调用了所有 exception 之后,最后 立马调用了 completeUnitOfWork

  • 这就说明这个节点报错了,这个节点已经完成了,它不会再继续去渲染它的子节点了

  • 因为这个节点它已经出错了,再渲染它的子节点是没有任何意义

  • 所以在这里面,如果有一个节点,出错了,就会立马对它执行 completeUnitOfWork

  • 它走的就是 unwindWork 的流程了, 这个后续来看

你可能感兴趣的:(React,React,Native,react.js,前端,前端框架)