身为开发者,我们都希望应用稳定运行,没有任何bug。但这是不可能的,我们是人类,都会犯错。无论我们多么的仔细,写了多少自动化测试,总是会写出一些bug。最重要的是,当事故发生时,能够快速定位这个bug,并优雅地修复这个bug。
在最后一章,我们会看看React是如何进行错误处理的:
- 错误发生时,我们可以做什么?
- 在诸多错误处理方法中,有什么要注意的吗?
- 如何降低这些错误带来的风险?
为什么要在React中捕获错误
首先要搞明白的是,为什么需要在React中设置捕获错误的API?
原因很简单:自React 16以来,任何一个React 生命周期中的错误,都有可能导致整个应用被卸载。在这个版本之前,即使报错发生了,相关组件还是会留在页面,虽然有些功能可能已经不可用了。但是自React 16之后,哪怕是一个不起眼的UI的报错,或者一些外部库的不可控报错,都有可能破坏整个应用,给用户留下一个白屏。
JavaScript是如何捕获错误的?
我们可以使用try/catch
语法。try/catch
本身就说明其逻辑:在try
的块中执行一些代码,如果在此期间发生错误了,就在在catch
的块中处理这些错误:
js
try {
// if we're doing something wrong, this might throw an error
doSomething();
} catch (e) {
// if error happened, catch it and do something with it without topping the app
// like sending this error to some logging service
}
在处理异步操作时,也可以使用类似的语法:
js
try {
await fetch('/bla-bla');
} catch (e) {
// oh no, the fetch failed! We should do something about it!
}
或者,我们可以使用老学究风格的promise语法,在catch
中处理相关错误:
js
fetch('/bla-bla')
.then((result) => {
// if a promise is successful, the result will be here
// we can do something useful with it
})
.catch((e) => {
// oh no, the fetch failed! We should do something about it!
});
这两个方式的概念是一样的,就是在具体实现上有细微不同。所以在这一章剩下的部分,我主要使用try/catch
语法。
在React中使用try/catch
,以及注意事项
如果一个错误被捕获了,我们应该做点什么,而不仅仅留一个白屏幕给用户:
js
const SomeComponent = () => {
const [hasError, setHasError] = useState(false);
useEffect(() => {
try {
// do something like fetching some data
} catch (e) {
// oh no! the fetch failed, we have no data to render!
setHasError(true);
}
});
// something happened during fetch, lets render some nice errorscreen
if (hasError) return <SomeErrorScreen />;
// all's good, data is here, let's render it
return <SomeComponentContent {...datasomething} />;
};
这段代码很简单,在useEffect
中调用一个接口。如果在调用过程中发生了错误,设置hasError
状态为true,当hasError
为true,就会渲染<SomeErrorScreen />
组件,为用户提供运维人员的电话号码等。
这段代码是非常直观的,对于简单的fetch
来说,它是非常有效的。
但是如果你想捕获这个组件内所有的错误,那你就会发现这段代码的局限性了。
局限性1:在使用useEffect
时,会遇到一些麻烦
如果我们用try/catch
包裹住useEffect
,那么useEffect
将不再运作:
js
try {
useEffect(() => {
throw new Error('Hulk smash');
}, []);
} catch (e) {
// useEffect throws, but this will never be called
}
代码会这样运作的原因是,在React中,useEffect
是在完成页面渲染后,异步 地调用的,所以在try/catch
看来,useEffect
这段代码没有任何异常。同样的事情也会发生在Promise:如果我们不等待结果,那么 JavaScript 将会继续执行其后续操作,当 Promise 完成时再返回来处理,并且只会执行 useEffect 内部的代码(或者 Promise 的 then 回调中的代码)。而到那个时候,try/catch 块早就已经执行完毕并结束了。
为了能够捕获到useEffect
里发生的错误,try/catch
应该被放在useEffect
里:
js
useEffect(() => {
try {
throw new Error('Hulk smash');
} catch(e) {
// this one will be caught
}
}, []);
代码示例: advanced-react.com/examples/16...
这种写法适用于任何使用了useEffect
的钩子,或者其他异步允许的代码。同理,为了更好的捕获每一个异常,我们不应该使用单一try/catch
来包裹所有,而是针对每一个钩子拆分出try/catch
块来捕获异常。
局限性2: 子组件
try/catch
无法捕获子组件里发生的任何错误。你不能这样写:
js
const Component = () => {
let child;
try {
child = <Child />
} catch (e) {
// useless for catching errors inside Child component, won't be triggered
}
return child;
}
这样也不行:
js
const Component = () => {
let child;
try {
return <Child />
} catch (e) {
// still useless for catching errors inside Child component, won't be triggered
}
}
代码示例: advanced-react.com/examples/16...
这个try/catch
发生在我们写<Child />
时,此时我们并没有渲染这个组件。我们不过是生成了一个描述对象罢了。描述对象的本质,不过是描述了这个组件的类型、key还有必要属性的对象罢了。
而针对这个属性的渲染,是在try/catch
成功执行后发生的,就像刚刚的把useEffect
放在try/catch
块里面一样。
局限性3: 在没有渲染内容时设置状态
如果你试图在 useEffect 以及各种回调函数之外(也就是在组件的渲染过程中)捕获错误,那么要妥善处理这些错误就不再那么简单了:在渲染过程中是不允许进行状态更新的。
像下面的代码,将会在错误发生时,导致无限重新渲染:
js
const Component = () => {
const [hasError, setHasError] = useState(false);
try {
doSomethingComplicated();
} catch (e) {
// don't do that! will cause infinite loop in case of an error
// see codesandbox below with live example
setHasError(true);
}
}
代码示例: advanced-react.com/examples/16...
我们可以这样,在函数全局内捕获到错误后,返回一个<SomeErrorScreen />
组件,而非调用setState
。
js
const Component = () => {
try {
doSomethingComplicated();
} catch (e) {
// this allowed
return <SomeErrorScreen />;
}
}
但正如你可以想象的那样,这有点麻烦,并且迫使我们在同一个组件中以不同的方式处理错误:对于 useEffect 和回调函数使用状态来处理错误,而对于其他所有情况则直接返回。
js
// while it will work, it's super cumbersome and hard to maitain, don't do that
const SomeComponent = () => {
const [hasError, setHasError] = useState(false);
useEffect(() => {
try {
// do something like fetching some data
} catch (e) {
// can't just return in case of errors in useEffect or callbacks
// so have to use state
setHasError(true);
}
});
try {
// do something during render
} catch (e) {
// but here we can't use state, so have to return directly in case of an error
return <SomeErrorScreen />;
}
// and still have to return in case of error state here
if (hasError) return <SomeErrorScreen />;
return <SomeComponentContent {...datasomething} />;
};
概括这个章节:如果我们仅仅是依赖try/catch
来开发React项目,我们将会无法捕获大部分的报错,或者把每一个组件写的过于复杂难懂,甚至因此带来其他报错...
好在,还有另一方法!!!