从页面死循环“命案”,探究React边界错误
事故现场前夕
不妨先来看看子组件
发生异常后,没有捕获错误的情况:
1 | # 第一步:程序正常运行,自组件计时器开始运行 |
可以比较清晰的看到,页面是挂掉了,但是对于正常情况下,这样的情况是很不友好的,当页面组件足够多的时候,可能只是某一块内容挂掉而导致把全局全部挂掉,不是特别理想。
事故现场
为什么会死循环?什么导致的死循环?怎么避免呢?
划重点:当父组件使用了getDerivedStateFromError
去处理捕获异常的时候,这时候没有后备渲染,就会导致异常无效,进而陷入死循环,更新<===>报错的往复
1 | class ErrorBoundary extends React.Component{ |
事故溯源
什么是错误边界?
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
异常分类
先介绍下
React 16之后的两个核心调度阶段
:render
&commit
阶段:图片来源于:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
而异常分类也发生在这两个阶段,且会有不同的表现:
Render
阶段的异常捕获流程有必要释疑上面无备用渲染时的场景,先上代码,后上动图。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// Child 组件模拟 render 阶段异常
const BugCounter = () => {
const [state, setState] = useState(0);
useEffect(() => {
// setInterval(() => {
// setState(pre => pre += 1);
// }, 1000);
}, []);
useEffect(() => {
console.log(state, "commit 阶段")
})
// if (state === 3) {
// // Simulate a JS error
// throw new Error('I crashed!');
// }
setTimeout(() => {
throw new Error('I crashed!');
}, 1000)
return (
<span>{state}</span>
);
};可以很明显看到,整个视图界面不会崩溃ℳ
Commit
阶段的异常捕获流程
CARE:
Render
阶段注意点:父组件同时定义 getDerivedStateFromError 和 componentDidCatch,如果使用 getDerivedStateFromError 处理异常信息时,没有启用备用渲染, componentDidCatch 不会触发, 异常处理无效,最后还是使用 console.error 打印异常信息。
这一点作一下特别解释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// BugCounter 组件(也即我们的子组件)
const BugCounter = () => {
const [state, setState] = useState(0);
useEffect(() => {
// setInterval(() => {
// setState(pre => pre += 1);
// }, 1000);
}, []);
useEffect(() => {
console.log(state, "commit 阶段")
})
//
// if (state === 3) {
// // Simulate a JS error
// throw new Error('I crashed!');
// }
throw new Error('I crashed!');
return (
<span>{state}</span>
);
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// ErrorBoundary (异常捕获组件,注意:这里并没有开启“备用渲染”)
class ErrorBoundary extends React.Component{
static getDerivedStateFromError(err) {
console.log(err, 'getDerivedStateFromError')
return {err};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.log(error, errorInfo, "didCatch")
}
render() {
return this.props.children;
}
}使用 getDerivedStateFromError 处理子组件异常信息时,要启用备用渲染,否则异常处理无效,最后还是使用 console.error 打印异常信息。
Commit
阶段注意点:子组件 commit 阶段发生异常,如果父组件定义 getDerivedStateFromError 且没有启用备用渲染,会导致异常处理无效,而且会陷入死循环。
Commit & Render
阶段注意点:子组件发生异常,react 会寻找离它最近的且定义getDerivedStateFromError、componentDidCatch 的父组件进行异常处理。
子组件发生异常,如果父组件没有捕获措施,react 会使用 console.error 打印异常信息。
事故补救
当同一种异常在ErrorTimeLimit秒内出现次数大于ErrorNumLimit次时显示错误页面,中断页面显示
大白话讲就是不想让页面一报错就崩掉。
但是,如果错误的异常请求是 XHR,且时间各为一秒,就会几率性绕过补救
最合适的方案:给页面内的组件最小粒度化添加错误边界
1 | import React, { FC } from 'react'; |
这里直接用了 Sentry
封装好的针对于 React
错误边界的组件;