自从接入了sentry,某些js没考虑到的错误而导致整个页面渲染白屏的,或者导致应用不能使用的...实在是影响用户体验,因此一顿检索后得知竟有如此宝物——错误边界!
错误边界(Error Boundaries)
概念介绍
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
实际上如果一个class组件拥有 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期中的一个,那这个组件就是错误边界
使用场景
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃。 为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
实现一个最简单的错误边界
// ErrorBoundary.tsx
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) {
return <div>错误展示的UI</div>;
}
return this.props.children;
}
}
// app.tsx
class App extends React.Component {
componentDidMount() {
// Uncaught ReferenceError: test is not defined
console.log(test)
}
render() {
<div>app</div>
}
}
你可以把它当成一个普通的组件去使用
<ErrorBoundary>
<App />
</ErrorBoundary>
注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,这也类似于 JavaScript 中 catch {} 的工作机制。
能处理的场景
- componentDidMount(上面已演示)
- componentDidUpdate
class App extends React.Component {
componentDidUpdate() {
// Uncaught ReferenceError: test is not defined
console.log(test);
}
render() {
return <div>classChild</div>
}
}
- componentWillUnmount
class App extends React.Component {
componentWillUnmount() {
// Uncaught ReferenceError: test is not defined
console.log(test);
}
render() {
return <div>classChild</div>
}
}
- useEffect(回调函数会被错误边界捕获)
function App() {
useEffect(() => {
// Uncaught ReferenceError: test is not defined
console.log(test);
return ()=> {
console.log(test)
}
}, [])
}
我们看到 App组件的的useEffect销毁函数并不会被错误边界捕获到,其实是因为useEffect是异步调度的,等到执行销毁函数的时候此时父级的fiber已经置空了,自然就找不到错误边界了。
- useLayoutEffect(回调函数和销毁函数都会被捕获)
function App() {
useLayoutEffect(() => {
// Uncaught ReferenceError: test is not defined
console.log(test);
return ()=> {
// Uncaught ReferenceError: test is not defined
console.log(test)
}
}, [])
}
useLayoutEffect 的销毁函数也是同步执行的,且其销毁函数在置空父级fiber之前,所以都能捕获到。
不能处理的场景
- 无法捕获事件处理器内部的错误。 如果需要在事件处理器中捕获错误,请使用 try/catch 语句
function App() {
function handleClick() {
console.log(test)
}
return (
<button onClick={handleClick}>click me</button>
)
}
- 组件外的错误
console.log(test)
function App() {
return (
<div>app</div>
)
}
- 在上述能捕获的生命周期中使用异步代码(例如
setTimeout或requestAnimationFrame回调函数)
function App() {
useLayoutEffect(() => {
setTimeout(()=> {
console.log(test);
}, 0)
}, [])
}
- 它自身抛出来的错误(并非它的子组件)
- 服务端渲染
总结
错误边界就是具备static getDerivedStateFromError() 或 componentDidCatch()其中一个生命周期方法的组件,它可以捕获发生在其子组件树在渲染期间、生命周期方法和整个组件树的构造函数的JS错误并渲染出备用UI。