5

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

 2 years ago
source link: https://segmentfault.com/a/1190000041057856
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

大家好,我卡颂。

本文会讲解ReactError Boundaries的完整实现逻辑。

一张图概括:

这里简单讲解下React工作流程,后文有用。分为三步:

  1. 触发更新
  2. render阶段:计算更新会造成的副作用
  3. commit阶段:在宿主环境执行副作用

副作用有很多,比如:

  • 插入DOM节点
  • 执行useEffect回调

好了,让我们进入主题。

欢迎加入人类高质量前端框架群,带飞

什么是Error Boundaries

React提供了两个与错误处理相关的API

  • getDerivedStateFromError:静态方法,当错误发生后提供一个机会渲染fallback UI
  • componentDidCatch:组件实例方法,当错误发生后提供一个机会记录错误信息

使用了这两个APIClassComponent通常被称为Error Boundaries(错误边界)。

Error Boundaries子孙组件中发生的所有React工作流程内的错误都会被Error Boundaries捕获。

通过开篇的介绍可以知道,React工作流程指:

  • render阶段
  • commit阶段

考虑如下代码:

class ErrorBoundary extends Component {
  componentDidCatch(e) {
    console.warn(“发生错误”, e);
  }
  render() {
    return <div>{this.props.children}</div>;
  }
}

const App = () => (
    <ErrorBoundary>
    <A><B/></A>
    <C/>
    <ErrorBoundary>
)

ABC作为ErrorBoundary的子孙组件,当发生React工作流程内的错误,都会被ErrorBoundary中的componentDidCatch方法捕获。

步骤1:捕获错误

首先来看工作流程中的错误都是何时被捕获的

render阶段的核心代码如下,发生的错误会被handleError处理:

do {
  try {
    // 对于并发更新则是workLoopConcurrent
workLoopSync();
    break;
  } catch (thrownValue) {
    handleError(root, thrownValue);
  }
} while (true);

commit阶段包含很多工作,比如:

  • componentDidMount/Update执行
  • 绑定/解绑ref
  • useEffect/useLayoutEffect callbackdestroy执行

这些工作会以如下形式执行,发生的错误被captureCommitPhaseError处理:

try {
// …执行某项工作 
} catch (error) {
  captureCommitPhaseError(fiber, fiber.return, error);
}

步骤2:构造callback

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

  • 如果存在Error Boundaries,执行对应API
  • 抛出React的提示信息
  • 如果不存在Error Boundaries,抛出未捕获的错误

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

一旦找到,就会构造:

  • 用于执行Error Boundaries APIcallback
  • 用于抛出React提示信息callback

React错误提示信息,包括提示语和错误堆栈

  // ...为了可读性,逻辑有删减
function createClassErrorUpdate() {
  if (typeof getDerivedStateFromError === 'function') {
// 用于执行getDerivedStateFromError的callback
    update.payload = () => {
      return getDerivedStateFromError(error);
};
// 用于抛出React提示信息的callback
    update.callback = () => {
      logCapturedError(fiber, errorInfo);
    };
  }
  if (inst !== null && typeof inst.componentDidCatch === 'function') {
// 用于执行componentDidCatch的callback
    update.callback = function callback() {
      this.componentDidCatch(error);
    };
  }
  return update;
}

如果没有找到Error Boundaries,继续向上遍历直到根节点。

此时会构造:

  • 用于抛出未捕获错误callback
  • 用于抛出React提示信息callback
// ...为了可读性,逻辑有删减
funffction createRootErrorUpdate() {
  // 用于抛出“未捕获的错误”及“React的提示信息”的callback
  update.callback = () => {
    onUncaughtError(error);
    logCapturedError(fiber, errorInfo);
  };
  return update;
}

执行callback

构造好的callback在什么时候执行呢?

React中有两个执行用户自定义callbackAPI

  • 对于ClassComponentthis.setState(newState, callback)newStatecallback参数都能传递Function作为callback

所以,对于Error Boundaries,相当于主动触发了一次更新:

this.setState(() => {
  // 用于执行getDerivedStateFromError的callback
}, () => {
  // 用于执行componentDidCatch的callback
  //  以及 用于抛出React提示信息的callback
})
  • 对于根节点,执行ReactDOM.render(element, container, callback)callback参数能传递Function作为callback

所以,对于没有Error Boundaries的情况,相当于主动执行了如下函数:

ReactDOM.render(element, container, () => {
// 用于抛出“未捕获的错误”及“React的提示信息”的callback
})

所以,Error Boundaries的实现可以看作是:React利用已有API实现的新功能。

经常有人问:为什么Hooks没有Error Boundaries

可以看到,Error Boundaries的实现借助了this.setState可以传递callback的特性,useState暂时无法完全对标。

最后,给你留个作业,在官方文档介绍了4种情况的错误不会被Error Boundaries捕获。

利用本文知识,你能分析下他们为什么不会被捕获么?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK