跳到主要内容

异常处理

2024年02月27日
柏拉文
越努力,越幸运

一、UI 渲染异常


1.1 认识

React 组件渲染过程如果有一个环节出现问题,就会导致整个组件渲染失败,那么整个组件的 UI 层都会显示不出来,这样造成的危害是巨大的,如果越靠近 APP 应用的根组件,渲染过程中出现问题造成的影响就越大,有可能直接造成白屏的情况。

为了防止渲染异常情况 React 增加了 componentDidCatchstatic getDerivedStateFromError() 两个额外的生命周期,去挽救由于渲染阶段出现问题造成 UI 界面无法显示的情况。

Error Boundaries(异常边界)是 React 组件,用于捕获它子组件树种所有组件产生的 js 异常,并渲染指定的兜底 UI 来替代出问题的组件。它能捕获子组件生命周期函数中的异常,包括构造函数(constructor)和 render 函数。而不能捕获以下异常:

  • Event handlers(事件处理函数)

  • Asynchronous code(异步代码,如setTimeout、promise.then等)

  • Server side rendering(服务端渲染)

  • Errors thrown in the error boundary itself (rather than its children)(异常边界组件本身抛出的异常)

之所以在以上异步场景中捕获不到, 是因为它们都不在渲染期间触发, 渲染期间try……catch 已经执行完成, 所以并不会捕获。

1.2 工作

React 内部其实也是通过 try...catch... 形式是捕获各阶段的异常,但是只在两个阶段的特定几处进行了异常捕获,这也是为什么异常边界只能捕获到子组件在构造函数、render 函数以及所有生命周期函数中抛出的异常。其中,throwExceptiondispatch 在遍历节点时,是从异常节点的父节点开始遍历,这也是为什么异常边界组件自身的异常不会捕获并处理

1.3 实现

import A from './components/A/a';
import B from './components/B/b';
import ErrorBoundary from './components/ErrorBoundary/errorBoundary';

function App() {
return (
<ErrorBoundary>
<A></A>
<B></B>
</ErrorBoundary>
);
}

export default App;

二、JS 代码异常


2.1 认识

2.2 实现

import { Component, ErrorInfo, PropsWithChildren } from 'react';

type ErrorBoundaryProps = PropsWithChildren<{
onError?: (error: Error, info: ErrorInfo) => void;
}>;

type ErrorBoundaryState = {
didCatch: boolean;
error: Error | null;
};

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { error: null, didCatch: false };
}

componentDidMount(): void {
window.addEventListener(
'error',
event => {
console.log('event error', event);
event.preventDefault();
this.setState({ error: event.error, didCatch: true });
this.props?.onError?.(event.error, { componentStack: '' });
},
true
);
window.addEventListener(
'rejectionhandled',
event => {
console.log('event rejectionhandled', event);
event.preventDefault();
this.setState({ error: event.reason, didCatch: true });
this.props?.onError?.(event.reason, { componentStack: '' });
},
false
);
window.addEventListener(
'unhandledrejection',
event => {
console.log('event unhandledrejection', event);
event.preventDefault();
this.setState({ error: event.reason, didCatch: true });
this.props?.onError?.(event.reason, { componentStack: '' });
},
false
);
}

render() {
const { didCatch, error } = this.state;

if (didCatch) {
return (
<div>
<h2>Something went wrong:</h2>
{error?.message}
</div>
);
}

return this.props.children;
}
}

export default ErrorBoundary;

三、全局异常捕获、处理


import { Component, ErrorInfo, PropsWithChildren } from 'react';

type ErrorBoundaryProps = PropsWithChildren<{
onError?: (error: Error, info: ErrorInfo) => void;
}>;

type ErrorBoundaryState = {
didCatch: boolean;
error: Error | null;
};

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { error: null, didCatch: false };
}

static getDerivedStateFromError(error: Error) {
return { error, didCatch: true };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.props?.onError?.(error, errorInfo);
}

componentDidMount(): void {
window.addEventListener(
'error',
event => {
console.log('event error', event);
event.preventDefault();
this.setState({ error: event.error, didCatch: true });
this.props?.onError?.(event.error, { componentStack: '' });
},
true
);
window.addEventListener(
'rejectionhandled',
event => {
console.log('event rejectionhandled', event);
event.preventDefault();
this.setState({ error: event.reason, didCatch: true });
this.props?.onError?.(event.reason, { componentStack: '' });
},
false
);
window.addEventListener(
'unhandledrejection',
event => {
console.log('event unhandledrejection', event);
event.preventDefault();
this.setState({ error: event.reason, didCatch: true });
this.props?.onError?.(event.reason, { componentStack: '' });
},
false
);
}

render() {
const { didCatch, error } = this.state;

if (didCatch) {
return (
<div>
<h2>Something went wrong:</h2>
{error?.message}
</div>
);
}

return this.props.children;
}
}

export default ErrorBoundary;