react源码 - Error Boundaries的实现

684 阅读3分钟

Error Boundaries

  • 可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI

usage

  • class 组件的两个生命周期函数(componentDidCatch | getDerivedStateFromError)
  1. 只有 getDerivedStateFromError 存在才会渲染 fallback UI吗?

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

原理: 发生错误重新构造更新对象并重新发起调度.

  • 不管是handleError还是captureCommitPhaseError,都会从发生错误的节点的父节点开始,逐层向上遍历,寻找最近的Error Boundaries
  • 存在Error Boundaries
  1. 构造 执行Error Boundaries API的callback
  2. 构造 抛出React提示信息callback

render phase (handleError)

  • demo
function ErrorComp() {
	throw new Error('error happen here')
    return <div>
    </div>
}
handleError: 异常处理
  • renderRootSync中 会通过try/catch 捕获 同步执行工作循环的主函数(workLoopSync)的异常
  do {
    try {
      workLoopSync();
      break;
    } catch (thrownValue) {
      // render阶段异常处理
      handleError(root, thrownValue);
    }
  } while (true);
throwException : 创建错误的更新对象
  • 首先将当前fiber标记为Incomplete , 然后从发生错误的fiber节点一直往上找, 直到找到最近的Error Boundaries, 并创建错误的更新对象(createClassErrorUpdate getDerivedStateFromError作为payload ,componentDidCatch作为callback),并将其添加的fiber.updateQueue中.
// createClassErrorUpdate
  if (typeof getDerivedStateFromError === 'function') {
    const error = errorInfo.value;
    // render阶段异常处理: 创建 update.payload
    update.payload = () => {
      logCapturedError(fiber, errorInfo);
      // 调用 getDerivedStateFromError
      return getDerivedStateFromError(error);
    };
  }
  
  if (inst !== null && typeof inst.componentDidCatch === 'function') {
    update.callback = function callback() {
      const error = errorInfo.value;
      const stack = errorInfo.stack;
      this.componentDidCatch(error, {
        componentStack: stack !== null ? stack : '',
      });
    };
  }
completeUnitOfWork: 找到 Error Boundaries 并作为 WIP 返回
  • erroredWork.flags === Incomplete: 将 erroredWork的所有returnFiber标记为 Incomplete,让其进入 (completedWork.flags & Incomplete) !== NoFlags的逻辑, 直到找到 Error Boundaries 并作为 workInProgress 返回(注意这里还会添加 DidCapture flag).重新进入到Reconciler阶段
updateClassComponent: 调用 getDerivedStateFromError 生命周期函数
  • 如果 getDerivedStateFromError存在 , 则会在 updateClassComponent -> resumeMountClassInstance —> processUpdateQueue -> getStateFromUpdate 中将被处理.返回的状态将被合并到 state
   // getStateFromUpdate根据不同的update.tag ,得到新的state
   case CaptureUpdate: {
      workInProgress.flags =
        (workInProgress.flags & ~ShouldCapture) | DidCapture;
    }
    // Intentional fallthrough
    case UpdateState: {
      const payload = update.payload;
      let partialState;
      if (typeof payload === 'function') {
		// 在这里 payload 就为 getDerivedStateFromError
        partialState = payload.call(instance, prevState, nextProps);
  • 如果 componentDidCatch 存在, 则会作为 update.callback在commit阶段被处理 [[commit#layout commitLayoutEffects]]
    const callback = update.callback;
        if (callback !== null) {
          workInProgress.flags |= Callback;
          const effects = queue.effects;
          if (effects === null) {
            queue.effects = [update];
          } else {
            effects.push(update);
          }
    }
  • finishClassComponent : 当前fiber 如果 flags === DidCapture ,则表示是 Error Boundaries 组件, 且只有 getDerivedStateFromError 存在, 才会继续调和子fiber(reconcileChildren), 否则 卸载所有的子fiber(即nextChildren=null)
function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes,
) {
  // Refs should update even if shouldComponentUpdate returns false
  markRef(current, workInProgress);
  // render阶段异常处理: 组件是否是 Error Boundaries 
  const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;

  if (!shouldUpdate && !didCaptureError) {
    // Context providers should defer to sCU for rendering
    if (hasContext) {
      invalidateContextProvider(workInProgress, Component, false);
    }

    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  const instance = workInProgress.stateNode;

  // Rerender
  ReactCurrentOwner.current = workInProgress;
  let nextChildren;
  // render阶段异常处理: 如果不存在 getDerivedStateFromError 卸载所有的children
  if (
    didCaptureError &&
    typeof Component.getDerivedStateFromError !== 'function'
  ) {
	// 卸载所有的children
    nextChildren = null;
  } else {
      // render阶段异常处理: 存在 则调用 render方法得到新的children
      nextChildren = instance.render();
  }

  if (current !== null && didCaptureError) {
    forceUnmountCurrentAndReconcile(
      current,
      workInProgress,
      nextChildren,
      renderLanes,
    );
  } else {
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }
  
  workInProgress.memoizedState = instance.state;

  return workInProgress.child;
}

commit phase (captureCommitPhaseError)

  • demo
function ErrorComp() {
    useEffect(()=>{
        throw new Error('error happen here')
    },[])
    return <div>
    </div>
}
captureCommitPhaseError: 捕获 commit阶段出现的异常
  • 在 [[useEffect#commit 阶段]] , 调用 create的时候, 如果发生异常将被 captureCommitPhaseError 捕获
// flushPassiveEffectsImpl
  const create = effect.create;
	  try{
		effect.destroy = create();
      } catch (error) {
        captureCommitPhaseError(fiber, error);
      }
  • captureCommitPhaseError: 做的工作就是找到 Error Boundaries fiber并创建更新对象并调度更新,注意是从 fiberRoot开始调度进入 workLoopSync.
updateComponentInstance: 调用 getDerivedStateFromError 生命周期函数
  • 如果 getDerivedStateFromError存在 , 则会在 updateClassComponent -> updateClassInstance —> processUpdateQueue -> getStateFromUpdate 中将被处理.返回的状态将被合并到 state
commitLayoutEffectOnFiber: 调用 componentDidCatch生命周期函数
  • componentDidCatch: 将作为 [[commit#layout commitLayoutEffects]] setState的 callback 通过 commitUpdateQueue 调用
export function commitUpdateQueue<State>(
  finishedWork: Fiber,
  finishedQueue: UpdateQueue<State>,
  instance: any,
): void {
  // Commit the effects
  const effects = finishedQueue.effects;
  finishedQueue.effects = null;
  if (effects !== null) {
    for (let i = 0; i < effects.length; i++) {
      const effect = effects[i];
      const callback = effect.callback;
      if (callback !== null) {
        effect.callback = null;
        callCallback(callback, instance);
      }
    }
  }
}