目录
2021SC@SDUSC
谈谈错误边界的概念
在部分UI内部的JavaScript中可能捕获到异常,为了确保整个应用能够顺利运行,可以在组件树中找到发生异常的组件,利用错误边界组件捕获并打印组件中的错误,并且展示备用UI,不会继续渲染那些已经发生崩溃的子组件树,错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。为明晰相关概念,根据官方文档,这里给出不被错误边界所处理的种种情况:事件处理,异步代码(例如 setTimeout
或 requestAnimationFrame
回调函数),服务端渲染,它自身抛出来的错误(并非它的子组件,关于这一点,我会在以后的源码分析中进行描述)。
找到待处理节点
现在我想要讨论一下react中的错误边界问题,错误在renderRootConcurrent中抓取,源码:
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
lane: Lane,
): Update<mixed> {
const update = createUpdate(NoTimestamp, lane);
update.tag = CaptureUpdate;
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
if (typeof getDerivedStateFromError === 'function') {
const error = errorInfo.value;
update.payload = () => {
return getDerivedStateFromError(error);
};
update.callback = () => {
logCapturedError(fiber, errorInfo);
};
}
const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
update.callback = function callback() {
logCapturedError(fiber, errorInfo);
if (typeof getDerivedStateFromError !== '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 getDerivedStateFromError is
// not defined.
markLegacyErrorBoundaryAsFailed(this);
}
const error = errorInfo.value;
const stack = errorInfo.stack;
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : '',
});
};
}
return update;
}
接下来在处理中(主要是在beginWork中,其中好几种需要抛出的情况),在handleError的ThrowException中,首先为出错节点打上Incomplete的标签并在抛出时为有能力处理错误的节点打上shouldCapture,然后通过createClassErrorUpdate方法在updateQueue中放入代表该节点的update,其中update的payload为getDerivedStateFromError,而其中的callback中包含componentDidCatch。
{
workInProgress.flags |= ShouldCapture;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
// Schedule the error boundary to re-render using updated state
const update = createClassErrorUpdate(
workInProgress,
errorInfo,
lane,
);
enqueueCapturedUpdate(workInProgress, update);
return;
}
然后为该节点调用completeUnitOfWork,以结束当前节点的渲染。在completeUnitOfWork中,会首先检查该节点的属性是否为InComplete,如果未完成,就执行unwindWork,在unwindWork中会检查组件是否为错误边界。假如节点的属性并非shouldCapture,那么应该返回空值,当返回空值时,会将该节点的父节点标记为InComplete,然后移至父节点,继续循环向上查找直至找到属性为shouldCapture的节点为止。
{
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
const next = unwindWork(completedWork, subtreeRenderLanes);
// Because this fiber did not complete, don't reset its lanes.
if (next !== null) {
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its subtree flags.
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
}
}
在unwindwork中,关于classComponent的部分代码:
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
const flags = workInProgress.flags;
if (flags & ShouldCapture) {
workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
if (
enableProfilerTimer &&
(workInProgress.mode & ProfileMode) !== NoMode
) {
transferActualDuration(workInProgress);
}
return workInProgress;
}
return null;
}
找到错误边界
假如发现节点是错误边界,则取消shouldCapture属性并添加DidCapture属性并将该节点返回,此时将workInProgress指向该节点并且再次执行beginWork以及completeWork。而再次执行时会检测其DidCapture属性,在beginWork中如果检测到便触发更新队列中的错误处理函数。然后会渲染为相应错误处理组件(也就是错误边界所指定的组件)。注意,错误处理时总是找到其能够处理错误的祖先节点进行处理。另外,通过workInprogress树的指向来调整错误处理的位置,保证其能否准确找到需要处理的节点并进行处理。
在beginWork中对DidCapture属性的组件处理
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFromError is not defined,
// unmount all the children. componentDidCatch will schedule an update to
// re-render a fallback. This is temporary until we migrate everyone to
// the new API.
// TODO: Warn in a future release.
nextChildren = null;
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
setIsRendering(true);
nextChildren = instance.render();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
) {
setIsStrictModeForDevtools(true);
try {
instance.render();
} finally {
setIsStrictModeForDevtools(false);
}
}
setIsRendering(false);
} else {
nextChildren = instance.render();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
if (current !== null && didCaptureError) {
// If we're recovering from an error, reconcile without reusing any of
// the existing children. Conceptually, the normal children and the children
// that are shown on error are two different sets, so we shouldn't reuse
// normal children even if their identities match.
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderLanes,
);
} else {
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
在beginWork中,会对组件的DidCapture属性进行检测,一旦发现其DidCapture属性并且getDerivedStateFromError该函数未定义,那就对类组件中的子组件nextChildren命名为null,假如找到了对应的current节点,此时捕获异常的组件与展示组件一般情况下应该不同,所以并不重用其子节点,那么接下来就执行forceUnmountCurrentAndReconcile方法将其该current节点卸载并且进行协调。
总结
讲解了如何对一部分待处理的节点利用错误边界机制进行正确的处理,也就是发现在组件渲染中的异常之后react的部分具体流程,以便于在组件出现异常后,能够捕获异常渲染错误边界组件并且依旧能够渲染其他正常的组件,增强react渲染节点的稳健性。