Error Boundaries 学习笔记

778 阅读2分钟

react-reconciler 版本 0.26.1

本文是学习笔记。

学习文章:Error Boundaries是这么实现的,还挺巧妙

流程:

  1. 捕获错误

    render 阶段

    // renderRootSync
    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
    

    commit 阶段

    // commitRootImpl 
    do {
      try {
        commitBeforeMutationEffects();
      } catch (error) {
        invariant(nextEffect !== null, 'Should be working on an effect.');
        captureCommitPhaseError(nextEffect, error);
        nextEffect = nextEffect.nextEffect;
      }
    } while (nextEffect !== null);
    

    Error Boundaries是这么实现的,还挺巧妙 :

    可以发现,即使没有Error Boundaries,「工作流程」中的错误已经被React捕获了。而正确的逻辑应该是:

    1. 如果存在Error Boundaries,执行对应API

    2. 抛出React的提示信息

    3. 如果不存在Error Boundaries,抛出「未捕获的错误」

    所以,不管是handleError还是captureCommitPhaseError,都会从发生错误的节点的父节点开始,逐层向上遍历,寻找最近的Error Boundaries。

  2. 构建 update

    createRootErrorUpdate / createClassErrorUpdate

    function createRootErrorUpdate(
      fiber: Fiber,
      errorInfo: CapturedValue<mixed>,
      lane: Lane,
    ): Update<mixed> {
      const update = createUpdate(NoTimestamp, lane);
      // Unmount the root by rendering null.
      update.tag = CaptureUpdate;
      update.payload = {element: null};
      const error = errorInfo.value;
      update.callback = () => {
        onUncaughtError(error);
        logCapturedError(fiber, errorInfo);
      };
      return update;
    }
    
    
    function createClassErrorUpdate(
      fiber: Fiber,
      errorInfo: CapturedValue<mixed>,
      lane: Lane,
    ): Update<mixed> {
      const update = createUpdate(NoTimestamp, lane);
      update.tag = CaptureUpdate;
      const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
      if (typeof getDerivedStateFromError === 'function') {
        const error = errorInfo.value;
        update.payload = () => {
          return getDerivedStateFromError(error);
        };
        update.callback = () => {
          logCapturedError(fiber, errorInfo);
        };
      }
    
      const inst = fiber.stateNode;
      if (inst !== null && typeof inst.componentDidCatch === 'function') {
        update.callback = function callback() {
          logCapturedError(fiber, errorInfo);
          if (typeof getDerivedStateFromError !== 'function') {
            markLegacyErrorBoundaryAsFailed(this);
          }
          const error = errorInfo.value;
          const stack = errorInfo.stack;
          this.componentDidCatch(error, {
            componentStack: stack !== null ? stack : '',
          });
        };
      }
      return update;
    }
    
  3. 触发更新

    captureCommitPhaseError / captureCommitPhaseError

    function captureCommitPhaseErrorOnRoot(
      rootFiber: Fiber,
      sourceFiber: Fiber,
      error: mixed,
    ) {
      const errorInfo = createCapturedValue(error, sourceFiber);
      const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
      enqueueUpdate(rootFiber, update, (SyncLane: Lane));
      const eventTime = requestEventTime();
      const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
      if (root !== null) {
        markRootUpdated(root, SyncLane, eventTime);
        ensureRootIsScheduled(root, eventTime);
      }
    }
    
    export function captureCommitPhaseError(
      sourceFiber: Fiber,
      nearestMountedAncestor: Fiber | null,
      error: mixed,
    ) {
      if (sourceFiber.tag === HostRoot) {
        // Error was thrown at the root. There is no parent, so the root
        // itself should capture it.
        captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
        return;
      }
    
      while (fiber !== null) {
        if (fiber.tag === HostRoot) {
          captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
          return;
        } else if (fiber.tag === ClassComponent) {
          const ctor = fiber.type;
          const instance = fiber.stateNode;
          if (
            typeof ctor.getDerivedStateFromError === 'function' ||
            (typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance))
          ) {
            const errorInfo = createCapturedValue(error, sourceFiber);
            const update = createClassErrorUpdate(
              fiber,
              errorInfo,
              (SyncLane: Lane),
            );
            enqueueUpdate(fiber, update, (SyncLane: Lane));
            const eventTime = requestEventTime();
            const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
            if (root !== null) {
              markRootUpdated(root, SyncLane, eventTime);
              ensureRootIsScheduled(root, eventTime);
            }
            return;
          }
        }
        fiber = fiber.return;
      }
    }
    

    可以看到 update 构建好了之后,就调用了 enqueueUpdate 将这次更新放到更新队列中,并且发起了调度。这次更新后,将会更新 getDerivedStateFromError(error) 的返回值且会在 layout 阶段打印报错信息,如果 componentDidCatch 方法存在,将会触发调用。

参考

[Error Boundaries是这么实现的,还挺巧妙] mp.weixin.qq.com/s/pEBQdvZIP…

[createClassErrorUpdate] github.com/facebook/re…

[createRootErrorUpdate] github.com/facebook/re…

[renderRootSync] github.com/facebook/re…

[handleError] github.com/facebook/re…

[captureCommitPhaseErrorOnRoot] github.com/facebook/re…

[captureCommitPhaseError] github.com/facebook/re…

[commitRootImpl] github.com/facebook/re…