在 React源码解析之renderRoot概览 中提到了,当有异常抛出的时候,会执行completeUnitOfWork()
:
//捕获异常,并处理
catch (thrownValue)
{
//抛出可预期的错误
throwException(
root,
returnFiber,
sourceFiber,
thrownValue,
renderExpirationTime,
);
//完成对sourceFiber的渲染,
//但是因为已经是报错的,所以不会再渲染sourceFiber的子节点了
//sourceFiber 即报错的节点
workInProgress = completeUnitOfWork(sourceFiber);
}
注意:
在throwException()
中,会对报错的fiber
添加Incomplete
的effectTag
:
// The source fiber did not complete.
//effectTag 置为 Incomplete
//判断节点更新的过程中出现异常
sourceFiber.effectTag |= Incomplete;
本篇文章就来解析 React 是如何捕获并处理错误的
(1) 执行completeUnitOfWork()
后,在内部会判断effectTag
是否为Incomplete
:
//如果该节点没有异常抛出的话,即可正常执行
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
}else{
// 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(workInProgress, renderExpirationTime);
//如果next存在,则表示产生了新 work
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.
//更新其 effectTag,标记是 restart 的
next.effectTag &= HostEffectMask;
//返回 next,以便执行新 work
return next;
}
//如果父节点存在的话,重置它的 Effect 链,标记为「未完成」
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its effect list.
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
显然,effectTag
和Incomplete
进行逻辑与后,是不可能等于NoEffect
的,所以会执行else{ }
的情况
(2) else
情况中,先执行unwindWork
,如果返回的 next 不为null
的话,则执行
next.effectTag &= HostEffectMask
除去Incomplete
和ShouldCapture
的effectTag
,而保留DidCapture
的effectTag
,为什么next.effectTag &= HostEffectMask
是这个意思,请看下面的「补充」
(3) 如果父节点存在的话,也将父节点标记为Incomplete
,也就是说,如果该 fiber 节点报错的话,就不会执行completeWork
来更新节点,而是返回父节点,直到返回能处理该 error 的节点
补充:
① 逻辑与&
是如何计算的,请参考 前端小知识10点(2020.2.10) 第八点
② NoEffect
/DidCapture
/HostEffectMask
/Incomplete
/ShouldCapture
的值:
export const NoEffect = /* */ 0b000000000000; //0
// 渲染出错,捕获到错误信息
export const DidCapture = /* */ 0b000001000000; //64
// Union of all host effects
export const HostEffectMask = /* */ 0b001111111111; //1023
// 任何造成 fiber 的 work 无法完成的情况
export const Incomplete = /* */ 0b010000000000; //1024
// 需要处理错误
export const ShouldCapture = /* */ 0b100000000000; //2048
作用:
根据不同组件的类型和目标节点的effectTag
,判断返回该节点还是null
源码:
//根据不同组件的类型和目标节点的effectTag,判断返回该节点还是 null
function unwindWork(
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
switch (workInProgress.tag) {
//注意:只有ClassComponent和SuspenseComponent有ShouldCaptutre 的 sideEffect
//也就是说,只有 ClassComponent和SuspenseComponent能捕获到错误
case ClassComponent: {
//===暂时跳过===
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
//获取effectTag
const effectTag = workInProgress.effectTag;
//如果 effectTag 上有 ShouldCapture 的副作用(side-effect)的话,
//就将 ShouldCapture 去掉,加上 DidCapture 的副作用
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
//如果fiberRoot 节点捕获到错误的话,则说明能处理错误的子节点没有去处理
//可能是 React 内部的 bug
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
const effectTag = workInProgress.effectTag;
invariant(
(effectTag & DidCapture) === NoEffect,
'The root failed to unmount after an error. This is likely a bug in ' +
'React. Please file an issue.',
);
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
//即 DOM 元素,会直接返回 null
//也就是说,会交给父节点去处理
//如果父节点仍是 HostComponent 的话,会向上递归,直到到达ClassComponent
//然后让ClassComponent捕获 error
case HostComponent: {
// TODO: popHydrationState
popHostContext(workInProgress);
return null;
}
case SuspenseComponent: {
popSuspenseContext(workInProgress);
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
// Captured a suspense effect. Re-render the boundary.
return workInProgress;
}
return null;
}
case DehydratedSuspenseComponent: {
if (enableSuspenseServerRenderer) {
// TODO: popHydrationState
popSuspenseContext(workInProgress);
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
// Captured a suspense effect. Re-render the boundary.
return workInProgress;
}
}
return null;
}
case SuspenseListComponent: {
popSuspenseContext(workInProgress);
// SuspenseList doesn't actually catch anything. It should've been
// caught by a nested boundary. If not, it should bubble through.
return null;
}
case HostPortal:
popHostContainer(workInProgress);
return null;
case ContextProvider:
popProvider(workInProgress);
return null;
case EventComponent:
if (enableFlareAPI) {
popHostContext(workInProgress);
}
return null;
default:
return null;
}
}
解析:
(1) 结构比较简单,switch...case
,根据 fiber 对象的类型,进行不同的处理,但该方法默认返回null
(2)重点看下ClassComponent
和HostComponent
的情况:
① fiber
对象的tag
为HostComponent
的话,那么该 fiber是 DOM 标签元素(div、span...),并且直接 return null 了。
返回null的意思是,当前节点不具备处理错误的能力,只能交由父节点去处理,一直往上,直到找到能处理错误的节点,比如ClassComponent
② ClassComponent
是能够处理 error 的,它对 fiber 节点进行的操作是:
去掉ShouldCapture
,加上DidCapture
的effectTag
,这表示捕获到 error 了,然后返回该 fiber 节点
联系一、completeUnitOfWork
可知:
(1) throwException()
为报错的fiber
添加Incomplete
的effectTag
(2) completeUnitOfWork()
根据Incomplete
去执行unwindWork()
(3) 如果unwindWork()
返回 null 的话,则将父节点的 effectTag 添上Incomplete
(4) 如果unwindWork()
返回该 fiber 的话,说明该节点是ClassComponent
,能够处理 error,将该 fiber 作为completeUnitOfWork()
执行的结果返回(completeUnitOfWork()
不会做do...while
循环了)
(5) 返回到performUnitOfWork()
——>performUnitOfWork()
——>workLoop()
,由于返回的不为 null,则再次执行performUnitOfWork()
——>beginWork()
,由于是ClassComponent
,所以执行updateClassComponent()
——>finishClassComponent()
补充:
关于completeUnitOfWork()
,请看:
React源码解析之completeUnitOfWork
关于workLoop()
、performUnitOfWork()
和beginWork()
,请看:
React源码解析之workLoop
关于updateClassComponent()
,请看:
React源码解析之updateClassComponent(上)
React源码解析之updateClassComponent(下)
我们看下finishClassComponent()
关于错误捕获的源码
源码:
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderExpirationTime: ExpirationTime,
) {
//判断是否有错误捕获
const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;
let nextChildren;
//getDerivedStateFromError是生命周期api,作用是捕获 render error,详情请看:
//https://zh-hans.reactjs.org/docs/react-component.html#static-getderivedstatefromerror
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFrom catch 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.
//如果出现 error 但是开发者没有调用getDerivedStateFromError的话,就中断渲染
nextChildren = null;
}
//当 classComponent 内部的节点报错时
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.
//强制重新计算 children,因为当出错时,是渲染到节点上的 props/state 出现了问题,所以不能复用,必须重新 render
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
}
}
解析:
可以看到当有DidCapture
的 effectTag 时,会执行forceUnmountCurrentAndReconcile()
源码:
// 强制重新计算 children
function forceUnmountCurrentAndReconcile(
current: Fiber,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
// This function is fork of reconcileChildren. It's used in cases where we
// want to reconcile without matching against the existing set. This has the
// effect of all current children being unmounted; even if the type and key
// are the same, the old child is unmounted and a new child is created.
//
// To do this, we're going to go through the reconcile algorithm twice. In
// the first pass, we schedule a deletion for all the current children by
// passing null.
//关于reconcileChildFibers()的讲解,请看「React源码解析之FunctionComponent(上)」
//https://juejin.im/post/5ddbe114e51d45231e010c75
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
//nextChildren 为 null 也就是删除内部的所有子节点
//渲染出的是一个空的 classComponent
null,
renderExpirationTime,
);
// In the second pass, we mount the new children. The trick here is that we
// pass null in place of where we usually pass the current child set. This has
// the effect of remounting all children regardless of whether their their
// identity matches.
//再渲染一遍,此时老 props 为 null(对应上面的 nextChildren = null)
workInProgress.child = reconcileChildFibers(
workInProgress,
//workInProgress 为 null
null,
//这里的新 props 跟老 props(null)基本是没有共同属性的
nextChildren,
renderExpirationTime,
);
}
解析:
连续执行两个reconcileChildFibers()
,更新时,将内部节点全部删除,目的是不渲染项目页面
此时会catch
到thrownValue
,那么就会返回到「前言」所说的源码上,再次执行throwException()
,让ClassComponent
渲染出捕获 error 的 ui 页面
补充:
关于reconcileChildFibers()
,请看:
React源码解析之FunctionComponent(上)
比较绕,逻辑是:
当有一个节点 throwError 后,给该节点一个Incomplete
的 effectTag,但只有ClassComponent
能捕获错误,所以会一层层向上找ClassComponent
,并给每个父级添加Incomplete
的 effectTag,直到找到ClassComponent
后,清空它的子节点(也就是不渲染出项目页面),并再次 throwError,此时React 会调用throwException()
,对ClassComponent
节点进行处理,逐层渲染出catch error的 ui 页面。
ReactFiberUnwindWork.js
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberUnwindWork.js