摘要
JavaScript 的错误会破坏 React 的内部状态,进而导致整个应用崩溃。为了解决这个问题,React 引入了错误边界。错误边界可以捕获子组件的 JavaScript 错误,打印这些错误并展示降级 UI。通过阅读本文,你可以了解到 ErrorBounary 使用方式和原理。文章的最后,提供了在线代码。
在线运行代码:codesandbox.io/s/react-sus…
介绍
过去,在使用 React 开发时遇到组件内部的任何 JavaScript 错误,都会破坏 React 的内部状态导致页面崩溃。例如,我们声明了一个组件 TriggleError,从 props 中解构 data 对象并展示 data.name。实际上 props 里并没有 data:
// TriggleError.jsx
import React from "react";
const TriggleError = (props) => {
const { data } = props;
return <div>{data.name}</div>;
};
export default TriggleError;
JavaScript 的报错导致页面奔溃,对于开发者和使用者来说都难以接受。为了解决这一问题,React 16 引入了一个新的概念 ErrorBounary(错误边界)来处理错误。ErrorBounary 特性:
- ErrorBounary 是一种 React 组件,可以在应用程序的任何位置捕获 JavaScript 错误,打印这些错误,并显示降级 UI。
- ErrorBounary 不会破坏应用程序的组件树,只会在组件中发生错误是展示降级UI。
- ErrorBounary 可以捕获子组件树渲染期间、生命周期方法以及构造函数中的错误。
但它并非万能,无法捕获以下场景中产生的错误:
- 事件处理
- 异步代码(例如
setTimeout
或requestAnimationFrame
回调函数) - 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
在上篇 「React 技巧」: Suspense 中提到,ErrorBounary 和 Suspense 相似。ErrorBounary 是用于捕获组件的错误,Suspense 时捕获组件的 Promise 异步状态。
方法
ErrorBounary Use Cases
在 React class 组件中,定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个时,那么它就变成一个ErrorBounary。当抛出错误后,使用 getDerivedStateFromError() 渲染备用 UI ,通过 componentDidCatch() 打印错误信息。例如,想要展示 Something went wrong!
的降级 UI,在 TriggleError 组件外层:
// CatchError.jsx
import React from "react";
import TriggleError from "./TriggleError";
import ErrorBoundary from "./ErrorBounary";
const CatchError = () => {
return (
<ErrorBoundary>
<TriggleError />
</ErrorBoundary>
);
};
export default CatchError;
可以使用已有的 JS库 react-error-bounary 替代自己实现。
// ErrorBounary.jsx
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
console.log(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong!</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
ErrorBounary 无法捕捉事件处理的错误,但我们可以通过 try catch 来处理事件中的错误,保证程序的健壮性:
import React from "react";
const TriggleError = (props) => {
const { data } = props;
console.log("data", props.data);
const handleClick = () => {
try {
// 执行操作,如有错误则会抛出
} catch(e) {
// pass
}
};
return <div onClick={handleClick}>{data.name}</div>;
};
export default TriggleError;
ErrorBounary Mechanism
ErrorBounary 的源码已经在上面给出,通过 getDerivedStateFromError 在 react render 阶段调用更新 state 来显示降级 UI。通过 componentDidCatch 在 react commit 阶段调用,执行上报错误日志等副作用。
- Find ErrorBounary: 从抛出异常的 Fiber 节点开始向上遍历,寻找 ErrorBounary;如果没找到,交给根节点来处理;
- ErrorBounary ?捕获错误:交给根节点:如果有ErrorBounary,创建一个 payload 为 getDerivedStateFromError 方法更新 state 值、 callback 为 componentDidCatch 的更新任务;如果是由根节点来处理异常,则创建一个卸载整个组件树的更新任务。
- Render: 进入处理异常的节点的 render 过程中,执行 performUnitOfWork,在该过程中会执行刚刚创建的更新任务。
- Render: 错误边界来处理异常,渲染降级 UI;由根节点来处理异常,则会卸载掉整个组件树,导致白屏。
根据以上四步结合源码分析:
- Find ErrorBounary
do {
try {
workLoopSync(); // workLoopSync中会调用beginWork
break;
} catch (thrownValue) {
handleError(root, thrownValue); // 处理异常
}
} while (true);
handleError 通过循环来寻找 ErrorBounary 或找到根节点:
- 当前节点或当前节点的父节点为 null,说明没有 ErrorBounary,结束循环;
- throwException 向父组件遍历,寻找 ErrorBounary;
- 执行 completeUnitOfWork,处理错误流程。
function handleError(root, thrownValue): void {
do {
let erroredWork = workInProgress;
try {
...
if (erroredWork === null || erroredWork.return === null) {
// 没有 ErrorBounary
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
workInProgress = null;
return;
}
...
// 向父组件遍历
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
// 处理信息
completeUnitOfWork(erroredWork);
} catch (yetAnotherThrownValue) {
..
}
return;
} while (true);
}
- 给节点的 EffectTag 标记为 Incomplete,进入异常处理逻辑;
- 向父组件遍历,当组件是 ClassComponent,且包含 getDerivedStateFromError 和 componentDidCatch 两者或其中之一,认为该节点是 ErrorBounary;
- 并给该节点打上 ShouldCapture 的 EffectTag,进入异常处理逻辑;分配一个最高优的 lane,保证本次 render 执行。
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed, // 异常本身
rootRenderLanes: Lanes,
) {
// 标记
sourceFiber.effectTag |= Incomplete;
sourceFiber.firstEffect = sourceFiber.lastEffect = null;
renderDidError();
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
// 根节点处理错误
case HostRoot: {
const errorInfo = value;
workInProgress.effectTag |= ShouldCapture;
// render
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return;
}
case ClassComponent:
const errorInfo = value;
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.effectTag & DidCapture) === NoEffect &&
(typeof ctor.getDerivedStateFromError === 'function' ||
(instance !== null &&
typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
// ErrorBounary 处理错误,标记
workInProgress.effectTag |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createClassErrorUpdate(
workInProgress,
errorInfo,
lane,
);
enqueueCapturedUpdate(workInProgress, update);
return;
}
break;
default:
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
- ErrorBounary ?捕获错误:交给根节点
当找到ErrorBounary时,调用 createClassErrorUpdate 将 payload 设置为 getDerivedStateFromError,callback 设置为 componentDidCatch。
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
lane: Lane,
): Update<mixed> {
const update = createUpdate(NoTimestamp, lane, null);
update.tag = CaptureUpdate;
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
if (typeof getDerivedStateFromError === 'function') {
const error = errorInfo.value;
update.payload = () => {
logCapturedError(fiber, errorInfo);
return getDerivedStateFromError(error);
};
}
const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
update.callback = function callback() {
if (typeof getDerivedStateFromError !== 'function') {
}
const error = errorInfo.value;
const stack = errorInfo.stack;
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : '',
});
};
}
return update;
}
未找到 ErrorBounary,根节点卸载组件树:
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
lane: Lane,
): Update<mixed> {
const update = createUpdate(NoTimestamp, lane, null);
update.tag = CaptureUpdate;
// 将根节点置为null,即卸载整棵React组件树
update.payload = {element: null};
const error = errorInfo.value;
update.callback = () => {
// 打印错误信息
onUncaughtError(error);
logCapturedError(fiber, errorInfo);
};
return update;
}
3 & 4 render
上一步我们已经找到了 ErrorBounary / 根节点,接下来处理错误并重新渲染。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.effectTag & Incomplete) === NoEffect) {
...
} else {
// 判断当前节点是否能处理错误
const next = unwindWork(completedWork, subtreeRenderLanes);
if (next !== null) {
// render & 终止
next.effectTag &= HostEffectMask;
workInProgress = next;
return;
}
if (returnFiber !== null) {
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
根据 Fiber 节点的类型和 effectTag 是否包含上一步标记的 ShouldCapture 来判断该节点能否处理异常
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
switch (workInProgress.tag) {
case ClassComponent: {
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
// errorbounary
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
case HostRoot: {
const effectTag = workInProgress.effectTag;
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
default:
return null;
}
}
forceUnmountCurrentAndReconcile
通过 forceUnmountCurrentAndReconcile 先卸载子节点的 ReactElement 对象,再将 ErrorBounary 展示的子节点挂载,渲染降级 UI。
function forceUnmountCurrentAndReconcile(
current: Fiber,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
null,
renderLanes,
);
workInProgress.child = reconcileChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
}
结论
ErrorBounary 是 React 提供的组件,可以用来捕获子组件 UI 的 JavaScript 报错,展示降级 UI。ErrorBounary 无法捕获事件处理、异步代码、服务端渲染和组件本身的错误。在 React class 组件中,包含 getDerivedStateFromError 和 componentDidCatch 两者或其中之一,认为该组件是是 ErrorBounary。文章的最后一小节梳理了 React 实现 ErrorBounary 的机制,本质上是通过 try catch 来捕获组件的错误,向父组件遍历找到 ErrorBounary 或根组件。ErrorBounary 会展示降级 UI,根组件则直接卸载组件树,展示空白页面。
在线运行代码:codesandbox.io/s/react-sus…
参考
[1] zh-hans.reactjs.org/docs/error-…
[2] imshubhamkhandal.medium.com/error-bound…
[3] zh-hans.reactjs.org/docs/error-…
[5] zh-hans.reactjs.org/docs/react-…
[6] zh-hans.reactjs.org/docs/react-…