React渲染错误啦~ 快看看我...错误边界(Error Boundaries)

481 阅读3分钟

自从接入了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。