基础-异常处理
本文主要是对React16异常处理部分的分析.
React16 引入了Error ErrorBoundaries, 即异常边界概念, 以及一个新的生命周期函数: componentDidCatch, 来支持React运行时的异常捕获和处理.
对 React 16 Error ErrorBoundaries, 主要从两个方面介绍:
- Error Boundaries的介绍和使用
- 源码分析
Error Boundaries(异常边界)
异常边界的引入是为了避免React的组件内的UI异常导致整个应用的异常.
Error Boundaries(异常边界)是React组件,用于捕获它子组件树种所有组件产生的js异常,并渲染指定的兜底UI来替代出问题的组件.
它能捕获子组件生命周期函数中的异常, 包括构造函数和render函数. 但不能捕获:
- 事件处理函数
- 异步代码,如setTimeout、promise等
- 服务端渲染
- 异常边界组件本身抛出的异常
具体的写法如下:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
使用如下:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
当MyWidget组件在构造函数, render函数以及所有生命周期函数中抛出异常时, 异常将会被ErrorBoundary异常边界组件捕获, 执行componentDidCatch函数, 渲染对应的fallback UI代替MyWidget组件.
源码分析(react 16)
React的和兴模块分为两个阶段: reconcilication阶段和commit阶段.
reconcilication

这个阶段的核心的部分是第三部分, React组件部分的生命周期函数的调用以及通过Diff算法计算出所有更新工作都在第三部分进行的, 所以异常处理部分也是在这部分进行的.
commit阶段
函数的调用流程如下:

这个阶段主要进行的工作是拿到reconcilication阶段产出的所有更新工作, 提交这些工作并调用渲染模块渲染UI, 完成UI渲染之后, 会调用剩余的生命周期函数, 所以异常处理也会在这部分进行.

React异常处理在源码中的入口主要有两处:
reconciliation阶段的 renderRoot 函数,对应异常处理方法是throwExceptioncommit阶段的commitRoot函数,对应异常处理方法是dispatch
throwException
首先看看renderRoot函数中与异常处理相关的部分:
function renderRoot(
root: FiberRoot,
isYieldy: boolean,
isExpired: boolean,
): void {
...
do {
try {
workLoop(isYieldy);
} catch (thrownValue) {
if (nextUnitOfWork === null) {
// This is a fatal error.
didFatal = true;
onUncaughtError(thrownValue);
} else {
...
const sourceFiber: Fiber = nextUnitOfWork;
let returnFiber = sourceFiber.return;
if (returnFiber === null) {
// This is the root. The root could capture its own errors. However,
// we don't know if it errors before or after we pushed the host
// context. This information is needed to avoid a stack mismatch.
// Because we're not sure, treat this as a fatal error. We could track
// which phase it fails in, but doesn't seem worth it. At least
// for now.
didFatal = true;
onUncaughtError(thrownValue);
} else {
throwException(
root,
returnFiber,
sourceFiber,
thrownValue,
nextRenderExpirationTime,
);
nextUnitOfWork = completeUnitOfWork(sourceFiber);
continue;
}
}
}
break;
} while (true);
...
}
这部分代码就是在workLoop大循环外套了层try...catch, 在catch中判断当前的错误类型, 然后调用不同的异常处理方法.
有两种异常处理方法:
- RootError, 最后调用
onUncaughtError函数处理 - ClassError, 最后调用
componentDidCatch生命周期函数处理.
这两种方法处理流程基本是类似的, 我们主要看ClassError方法.
下面先看看throwException的源码:
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
renderExpirationTime: ExpirationTime,
) {
...
// We didn't find a boundary that could handle this type of exception. Start
// over and traverse parent path again, this time treating the exception
// as an error.
renderDidError();
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case HostRoot: {
const errorInfo = value;
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime = renderExpirationTime;
const update = createRootErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
);
enqueueCapturedUpdate(workInProgress, update);
return;
}
case ClassComponent:
case ClassComponentLazy:
// Capture and retry
const errorInfo = value;
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.effectTag & DidCapture) === NoEffect &&
((typeof ctor.getDerivedStateFromCatch === 'function' &&
enableGetDerivedStateFromCatch) ||
(instance !== null &&
typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime = renderExpirationTime;
// Schedule the error boundary to re-render using updated state
const update = createClassErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
);
enqueueCapturedUpdate(workInProgress, update);
return;
}
break;
default:
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
throwException函数主要分为两部分:
- 遍历当前异常节点的所有父节点, 找到对应的错误信息(错误名称, 调用栈等), 这部分代码上面没有展示
- 第二部分就是遍历当前异常节点的所有父节点, 判断各节点的类型, 主要还是上面提到的两种类型, 这里重点描述
ClassComponent类型, 判断该节点是否是异常边界组件(通过是否存在componentDidCatch生命周期函数等), 如果是找到异常边界组件, 则调用createClassErrorUpdate函数新建update, 并将此update放入此节点的异常更新队列中, 在后续更新中, 会更新此队列中的更新工作.
我们看看createClassErrorUpdate的源码:
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime,
): Update<mixed> {
const update = createUpdate(expirationTime);
update.tag = CaptureUpdate;
...
const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
update.callback = function callback() {
if (
!enableGetDerivedStateFromCatch ||
getDerivedStateFromCatch !== 'function'
) {
// To preserve the preexisting retry behavior of error boundaries,
// we keep track of which ones already failed during this batch.
// This gets reset before we yield back to the browser.
// TODO: Warn in strict mode if getDerivedStateFromCatch is
// not defined.
markLegacyErrorBoundaryAsFailed(this);
}
const error = errorInfo.value;
const stack = errorInfo.stack;
logError(fiber, errorInfo);
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : '',
});
};
}
return update;
}
可以看到, 此函数返回了一个update, 此update的callback最终会调用组件的componentDidCatch生命周期函数.
而update的callback最终会在commit阶段的commitAllLifeCycles函数中被调用.
以上就是reconcilication阶段的异常捕获到异常处理的流程, 可以知道此阶段是在workLoop大循环外套了层try..catch, 所以workLoop里所有的异常都能被异常边界组件捕获并且处理.
下面看看commit阶段的dispatch.
dispatch
function dispatch(
sourceFiber: Fiber,
value: mixed,
expirationTime: ExpirationTime,
) {
let fiber = sourceFiber.return;
while (fiber !== null) {
switch (fiber.tag) {
case ClassComponent:
case ClassComponentLazy:
const ctor = fiber.type;
const instance = fiber.stateNode;
if (
typeof ctor.getDerivedStateFromCatch === 'function' ||
(typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance))
) {
const errorInfo = createCapturedValue(value, sourceFiber);
const update = createClassErrorUpdate(
fiber,
errorInfo,
expirationTime,
);
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
return;
}
break;
case HostRoot: {
const errorInfo = createCapturedValue(value, sourceFiber);
const update = createRootErrorUpdate(fiber, errorInfo, expirationTime);
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
return;
}
}
fiber = fiber.return;
}
if (sourceFiber.tag === HostRoot) {
// Error was thrown at the root. There is no parent, so the root
// itself should capture it.
const rootFiber = sourceFiber;
const errorInfo = createCapturedValue(value, rootFiber);
const update = createRootErrorUpdate(rootFiber, errorInfo, expirationTime);
enqueueUpdate(rootFiber, update);
scheduleWork(rootFiber, expirationTime);
}
}
dispatch函数做的事情和上部分的throwException类似, 遍历当前异常节点的所有父节点, 找到异常边界组件, 新建update, 在update.callback中调用组件的componentDidCatch生命周期函数, 后续的部分就不详述, 基本与reconclilication阶段一直. 看看commit阶段那些部分调用了dispatch函数:
function captureCommitPhaseError(fiber: Fiber, error: mixed) {
return dispatch(fiber, error, Sync);
}
调用captureCommitPhaseError即调用dispatch, 而captureCommitPhaseError主要在commitRoot函数中被调用, 源码如下:
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
...
// commit阶段的准备工作
prepareForCommit(root.containerInfo);
// Invoke instances of getSnapshotBeforeUpdate before mutation.
nextEffect = firstEffect;
startCommitSnapshotEffectsTimer();
while (nextEffect !== null) {
let didError = false;
let error;
try {
// 调用 getSnapshotBeforeUpdate 生命周期函数
commitBeforeMutationLifecycles();
} catch (e) {
didError = true;
error = e;
}
if (didError) {
captureCommitPhaseError(nextEffect, error);
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}
stopCommitSnapshotEffectsTimer();
// Commit all the side-effects within a tree. We'll do this in two passes.
// The first pass performs all the host insertions, updates, deletions and
// ref unmounts.
nextEffect = firstEffect;
startCommitHostEffectsTimer();
while (nextEffect !== null) {
let didError = false;
let error;
try {
// 提交所有更新并调用渲染模块渲染UI
commitAllHostEffects(root);
} catch (e) {
didError = true;
error = e;
}
if (didError) {
captureCommitPhaseError(nextEffect, error);
// Clean-up
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}
stopCommitHostEffectsTimer();
// The work-in-progress tree is now the current tree. This must come after
// the first pass of the commit phase, so that the previous tree is still
// current during componentWillUnmount, but before the second pass, so that
// the finished work is current during componentDidMount/Update.
root.current = finishedWork;
// In the second pass we'll perform all life-cycles and ref callbacks.
// Life-cycles happen as a separate pass so that all placements, updates,
// and deletions in the entire tree have already been invoked.
// This pass also triggers any renderer-specific initial effects.
nextEffect = firstEffect;
startCommitLifeCyclesTimer();
while (nextEffect !== null) {
let didError = false;
let error;
try {
// 调用剩余生命周期函数
commitAllLifeCycles(root, committedExpirationTime);
} catch (e) {
didError = true;
error = e;
}
if (didError) {
captureCommitPhaseError(nextEffect, error);
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}
...
}
可以看到,有三处(也是commit阶段主要的三部分)通过try...catch...调用了 captureCommitPhaseError函数,即调用了 dispatch函数,而这三个部分具体做的事情注释里也写了.
刚刚我们提到,update的callback会在commit阶段的commitAllLifeCycles函数中被调用,我们来看下具体的调用流程:
- commitAllLifeCycles函数中会调用commitLifeCycles函数
- 在commitLifeCycles函数中,对于ClassComponent和HostRoot会调用commitUpdateQueue函数
- 我们来看看 commitUpdateQueue 函数源码:
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
...
// Commit the effects
commitUpdateEffects(finishedQueue.firstEffect, instance);
finishedQueue.firstEffect = finishedQueue.lastEffect = null;
commitUpdateEffects(finishedQueue.firstCapturedEffect, instance);
finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;
}
function commitUpdateEffects<State>(
effect: Update<State> | null,
instance: any,
): void {
while (effect !== null) {
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
effect = effect.nextEffect;
}
}
们可以看到,commitUpdateQueue函数中会调用两次commitUpdateEffects函数,参数分别是正常update队列以及存放异常处理update队列
而commitUpdateEffects函数就是遍历所有update,调用其callback方法
上文提到,commitAllLifeCycles函数中是用于调用剩余生命周期函数,所以异常边界组件的 componentDidCatch生命周期函数也是在这个阶段调用
小结
React内部其实也是通过try...catch形式捕获各阶段的异常, 但是只是在两个阶段的特定几处进行了异常捕获, 这也就是为什么异常边界只能捕获到子组件在构造函数, render函数以及所有生命周期函数中抛出异常.
注意到, throwException和dispatch在遍历节点的时候, 是从异常节点的父节点开始遍历的, 这也是为什么异常边界组件自身的异常不会捕获并且处理.
React内部将异常分为了两种异常处理方法:RootError、ClassError,我们只重点分析了 ClassError 类型的异常处理函数,其实 RootError 是一样的,区别在于最后调用的处理方法不同,在遍历所有父节点过程中,如果有异常边界组件,则会调用 ClassError 类型的异常处理函数,如果没有,一直遍历到根节点,则会调用 RootError 类型的异常处理函数,最后调用的 onUncaughtError 方法,此方法做的事情很简单,其实就是将 hasUnhandledError 变量赋值为 true,将 unhandledError 变量赋值为异常对象,此异常对象最终将在 finishRendering函数中被抛出,而finishRendering函数是在performWork函数的最后被调用.