React异常处理

在React 0.15版本及之前版本中,组件内的UI异常将中断组件内部状态,导致下一次渲染时触发隐藏异常。React并未提供友好的异常捕获和处理方式,一旦发生异常,应用将不能很好的运行。而React 16版本有所改进。本文主旨就是探寻React异常捕获的现状,问题及解决方案。

前言

我们期望的是在UI中发生的一些异常,即组件内异常(指React 组件内发生的异常,包括组件渲染异常,组件生命周期方法异常等),不会中断整个应用,可以以比较友好的方式处理异常,上报异常。在React 16版本以前是比较麻烦的,在React 16中提出了解决方案,将从异常边界(Error Boundaries)开始介绍。

异常边界

所谓异常边界,即是标记当前内部发生的异常能够被捕获的区域范围,在此边界内的JavaScript异常可以被捕获到,不会中断应用,这是React 16中提供的一种处理组件内异常的思路。具体实现而言,React提供一种异常边界组件,以捕获并打印子组件树中的JavaScript异常,同时显示一个异常替补UI。

组件内异常

组件内异常,也就是异常边界组件能够捕获的异常,主要包括:

  1. 渲染过程中异常;
  2. 生命周期方法中的异常;
  3. 子组件树中各组件的constructor构造函数中异常。

其他异常

当然,异常边界组件依然存在一些无法捕获的异常,主要是异步及服务端触发异常:

  1. 事件处理器中的异常;
  2. 异步任务异常,如setTiemout,ajax请求异常等;
  3. 服务端渲染异常;
  4. 异常边界组件自身内的异常;

异常边界组件

前面提到异常边界组件只能捕获其子组件树发生的异常,不能捕获自身抛出的异常,所以有必要注意两点:

  1. 不能将现有组件改造为边界组件,否则无法捕获现有组件异常;
  2. 不能在边界组件内涉及业务逻辑,否则这里的业务逻辑异常无法捕获;

很显然,最终的异常边界组件必然是不涉及业务逻辑的独立中间组件。

那么一个异常边界组件如何捕获其子组件树异常呢?很简单,首先它也是一个React组件,然后添加ComponentDidCatch生命周期方法。

实例

创建一个React组件,然后添加ComponentDidCatch生命周期方法:

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

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Meet Some Errors.</h1>;
    }
    return this.props.children;
  }
}

接下来可以像使用普通React组件一样使用该组件:

<ErrorBoundary>
  <App />
</ErrorBoundary>

ComponentDidCatch

这是一个新的生命周期方法,使用它可以捕获子组件异常,其原理类似于JavaScript异常捕获器try, catch

ComponentDidCatch(error, info)
  1. error:应用抛出的异常;

    异常对象

  2. info:异常信息,包含ComponentStack属性对应异常过程中冒泡的组件栈;

    异常信息组件栈

判断组件是否添加componentDidCatch生命周期方法,添加了,则调用包含异常处理的更新渲染组件方法:

if (inst.componentDidCatch) {
  this._updateRenderedComponentWithErrorHandling(
    transaction,
    unmaskedContext,
  );
} else {
  this._updateRenderedComponent(transaction, unmaskedContext);
}

_updateRenderedComponentWithErrorHandling里面使用try, catch捕获异常:

 /**
   * Call the component's `render` method and update the DOM accordingly.
   *
   * @param {ReactReconcileTransaction} transaction
   * @internal
   */
_updateRenderedComponentWithErrorHandling: function(transaction, context) {
  var checkpoint = transaction.checkpoint();
  try {
    this._updateRenderedComponent(transaction, context);
  } catch (e) {
    // Roll back to checkpoint, handle error (which may add items to the transaction),
    // and take a new checkpoint
    transaction.rollback(checkpoint);
    this._instance.componentDidCatch(e);     

    // Try again - we've informed the component about the error, so they can render an error message this time.
    // If