[React 源码] React 18.2 - ErrorBoundary | Suspense [0.8k 字 - 阅读时长2min]

511 阅读2分钟

ErrorBoundary

React 通过深度优先遍历来调度 Fiber ,所以 ErrorBoundary 组件 有能力捕获到渲染期间子组件发生的异常,并且会阻止异常继续传播,不会导致整个应用程序崩溃。

export default class ErrorBoundary extends React.Component<Props> {
  state = { hasError: false, error: null };
  static getDerivedStateFromError (error: any) {
    return {
      hasError: true,
      error,
    };
  }
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

当子组件发生异常时,会通过调用 getNearestErrorBoundaryID 找到距离发生异常的子组件,最近的 ErrorBoundary 组件,调用 getDerivedStateFromError 方法,将 this.state.hasError 置为 true, 则渲染到 ErrorBoundary 组件时,调用 render 函数,可以利用 hasError 组件状态,在 render 函数中定制子组件发生异常时的 UI

  function getNearestErrorBoundaryID(fiber: Fiber): number | null {
    let parent = fiber.return;
    while (parent !== null) {
      if (isErrorBoundary(parent)) {
        return getFiberIDUnsafe(parent);
      }
      parent = parent.return;
    }
    return null;
  }

Suspense

Suspense 的核心概念与 ErrorBoundary 非常相似,ErrorBoundary 是通过捕获其子组件的异常,来展示错误回滚的 UI。而 Suspense 是通过捕获子组件 抛出的 promise 来展示数据获取之前的 fallBack UI,数据获取之后展示 children UI

Suspense 解决问题是:极大简化复杂的 stuff, 比如 异步获取数据 (data fetching), 代码分割 (code splitting),以及在你的应用程序当中任何异步依赖的数据。Suspense 可以精准控制异步数据/组件的 Loading 状态。

Suspense 在 异步组件中的使用,就是和 React.lazy 结合,在异步组件获取到之前,显示 fallback UI。

export function lazy<T>(
  ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
  const payload: Payload<T> = {
    // We use these fields to store the result.
    _status: Uninitialized,
    _result: ctor,
  };
}

React.lazy 原理相当于就是在组件当中 throw 了一个 thenable 函数。

SuspenseLoading 中的使用,大家可以去看 React 核心成员 Dan 的 demo。Dan Abramov 的 frosty-hermann-bztrp

Suspense Loading

Suspense 使用

export default class extends Component {
  render() {
    return (
        <Suspense fallback={<h1>加载中</h1>}>
          <User />
        </Suspense>
      </ErrorBoundary>
    );
  }
}

User 组件在数据获取到之前,即状态为 pending 时会 throw 一个 promise

// Suspense 参数就是一个发送网络请求的 promise
function wrapperPromise(Suspense: Promise<any>) {
  let status = "pending";
  let result: any;
  return {
    read() {
      if (status === "success" || status === "error") {
        return result;
      } else {
        // throw 一个 promise
        throw promise.then(
          (data: any) => {
            status = "success";
            result = data;
          },
          (error: any) => {
            status = "error";
            result = error;
          }
        );
      }
    },
  };
}

这时, 距离该子组件最近的 Suspense 组件 componentDidCatch 生命周期中,捕获到了该 promise。将状态 loading 置为 true, 调度更新到 Suspense 组件时执行 render 函数时, loadingtrue 渲染 fallBack UI,跳过子组件。

export default class Suspense extends React.Component<
  SuspenseProps,
  SuspenseState
> {
  mounted: any = null;
  state = { loading: false };
  componentDidCatch(error: any) {
    if (typeof error.then === "function") {
      this.setState({ loading: true });
      error.then(() => {
        this.setState({ loading: false });
      });
    }
  }
  render() {
    const { fallback, children } = this.props;
    const { loading } = this.state;
    return loading ? fallback : children;
  }
}

等到 User 组件在数据获取到之后, 执行 then 回调函数中将状态 loading 置为 false, 再次调度更新时,loaingtrue 会直接渲染子组件。

componentDidCatch(error: any) {
  if (typeof error.then === "function") {
    this.setState({ loading: true });
    error.then(() => {
      this.setState({ loading: false });
    });
  }
}