1 )概述
completeWork
这个方法之后, 再次回到 renderRoot
里面renderRoot
里面执行了 workLoop
, 之后,对 workLoop
使用了try catchcompleteUnitOfWork
的时候,用来进行判断2 )源码
定位到 packages/react-reconciler/src/ReactFiberScheduler.js#L1293
在 renderRoot
里面的 do while 循环中
do {
try {
workLoop(isYieldy);
// 在catch里面得到了一个 thrownValue, 这是一个error
} catch (thrownValue) {
resetContextDependences();
resetHooks();
// Reset in case completion throws.
// This is only used in DEV and when replaying is on.
let mayReplay;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
mayReplay = mayReplayFailedUnitOfWork;
mayReplayFailedUnitOfWork = true;
}
// 如果 nextUnitOfWork 等于 null,这是一个不属于正常流程里面的一个情况
// 因为 nextUnitOfWork 是我们在更新一个节点之前,它是有值的更新
// 更新节点之后,它会被赋成一个新的值
// 一般来说它不应该是会存在 nextUnitOfWork 是 null 的一个情况
// 即便 throw error 了,上一个 nextUnitOfWork 也没有主动去把它消除
if (nextUnitOfWork === null) {
// This is a fatal error.
// 为 null 这种情况,被认为是一个致命的错误
didFatal = true;
// 调用 onUncaughtError,无法处理的错误被抛出了
// 这个时候,react是会直接中断渲染染流程
onUncaughtError(thrownValue);
} else {
// 非上述致命错误
if (enableProfilerTimer && nextUnitOfWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown.
// This avoids inaccurate Profiler durations in the case of a suspended render.
stopProfilerTimerIfRunningAndRecordDelta(nextUnitOfWork, true);
}
if (__DEV__) {
// Reset global debug state
// We assume this is defined in DEV
(resetCurrentlyProcessingQueue: any)();
}
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
if (mayReplay) {
const failedUnitOfWork: Fiber = nextUnitOfWork;
replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
}
}
// TODO: we already know this isn't true in some cases.
// At least this shows a nicer error message until we figure out the cause.
// https://github.com/facebook/react/issues/12449#issuecomment-386727431
invariant(
nextUnitOfWork !== null,
'Failed to replay rendering after an error. This ' +
'is likely caused by a bug in React. Please file an issue ' +
'with a reproducing case to help us find it.',
);
const sourceFiber: Fiber = nextUnitOfWork;
let returnFiber = sourceFiber.return;
// 如果 returnFiber 等于 null, 说明这个错误是出现在更新 RootFiber 的过程当中
// 那么 RootFiber 它是一个非常固定的节点,就是说它没有用户代码去进行一个参与的
// 如果这个它出现了一个错误,也是一个致命的错误,是一个react源码级的一个错误,同上一样处理
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);
进入 onUncaughtError
function onUncaughtError(error: mixed) {
invariant(
nextFlushedRoot !== null,
'Should be working on a root. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
// Unschedule this root so we don't work on it again until there's
// another update.
// 这时候直接把 nextFlushedRoot 设置成 NoWork,剩下的任务都不再执行了
nextFlushedRoot.expirationTime = NoWork;
// 处理全局变量
if (!hasUnhandledError) {
hasUnhandledError = true;
unhandledError = error;
}
}
进入 throwException
这个是常规处理错误的处理器
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
renderExpirationTime: ExpirationTime,
) {
// 增加 Incomplete 这个 SideEffect
// 这也是在 completeUnitOfWork 当中,去判断节点,是否有 Incomplete 的来源
// 只有在这个节点,throw了一个异常之后,它才会被赋值 SideEffect
// The source fiber did not complete.
sourceFiber.effectTag |= Incomplete;
// Its effect list is no longer valid.
// 对 firstEffect 和 lastEffect 置 null
// 因为它已经抛出一个异常了,子节点不会再进行渲染,不会有effect的一个链
sourceFiber.firstEffect = sourceFiber.lastEffect = null;
// 这种就匹配 Promise对象,或者 thenable 对象
// 这个其实就对应 Suspense 相关的处理,通过 throw 一个 thenable 的对象
// 可以让这个组件变成一个挂起的状态,等到这个 Promise 被 resolve之后,再次进入一个正常的渲染周期
// 这部分都是跟 Suspense 相关的代码,先跳过
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
// This is a thenable.
const thenable: Thenable = (value: any);
// Find the earliest timeout threshold of all the placeholders in the
// ancestor path. We could avoid this traversal by storing the thresholds on
// the stack, but we choose not to because we only hit this path if we're
// IO-bound (i.e. if something suspends). Whereas the stack is used even in
// the non-IO- bound case.
let workInProgress = returnFiber;
let earliestTimeoutMs = -1;
let startTimeMs = -1;
do {
if (workInProgress.tag === SuspenseComponent) {
const current = workInProgress.alternate;
if (current !== null) {
const currentState: SuspenseState | null = current.memoizedState;
if (currentState !== null && currentState.didTimeout) {
// Reached a boundary that already timed out. Do not search
// any further.
const timedOutAt = currentState.timedOutAt;
startTimeMs = expirationTimeToMs(timedOutAt);
// Do not search any further.
break;
}
}
let timeoutPropMs = workInProgress.pendingProps.maxDuration;
if (typeof timeoutPropMs === 'number') {
if (timeoutPropMs <= 0) {
earliestTimeoutMs = 0;
} else if (
earliestTimeoutMs === -1 ||
timeoutPropMs < earliestTimeoutMs
) {
earliestTimeoutMs = timeoutPropMs;
}
}
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
// Schedule the nearest Suspense to re-render the timed out view.
workInProgress = returnFiber;
do {
if (
workInProgress.tag === SuspenseComponent &&
shouldCaptureSuspense(workInProgress.alternate, workInProgress)
) {
// Found the nearest boundary.
// If the boundary is not in concurrent mode, we should not suspend, and
// likewise, when the promise resolves, we should ping synchronously.
const pingTime =
(workInProgress.mode & ConcurrentMode) === NoEffect
? Sync
: renderExpirationTime;
// Attach a listener to the promise to "ping" the root and retry.
let onResolveOrReject = retrySuspendedRoot.bind(
null,
root,
workInProgress,
sourceFiber,
pingTime,
);
if (enableSchedulerTracing) {
onResolveOrReject = Schedule_tracing_wrap(onResolveOrReject);
}
thenable.then(onResolveOrReject, onResolveOrReject);
// If the boundary is outside of concurrent mode, we should *not*
// suspend the commit. Pretend as if the suspended component rendered
// null and keep rendering. In the commit phase, we'll schedule a
// subsequent synchronous update to re-render the Suspense.
//
// Note: It doesn't matter whether the component that suspended was
// inside a concurrent mode tree. If the Suspense is outside of it, we
// should *not* suspend the commit.
if ((workInProgress.mode & ConcurrentMode) === NoEffect) {
workInProgress.effectTag |= CallbackEffect;
// Unmount the source fiber's children
const nextChildren = null;
reconcileChildren(
sourceFiber.alternate,
sourceFiber,
nextChildren,
renderExpirationTime,
);
sourceFiber.effectTag &= ~Incomplete;
if (sourceFiber.tag === ClassComponent) {
// We're going to commit this fiber even though it didn't complete.
// But we shouldn't call any lifecycle methods or callbacks. Remove
// all lifecycle effect tags.
sourceFiber.effectTag &= ~LifecycleEffectMask;
const current = sourceFiber.alternate;
if (current === null) {
// This is a new mount. Change the tag so it's not mistaken for a
// completed component. For example, we should not call
// componentWillUnmount if it is deleted.
sourceFiber.tag = IncompleteClassComponent;
}
}
// Exit without suspending.
return;
}
// Confirmed that the boundary is in a concurrent mode tree. Continue
// with the normal suspend path.
let absoluteTimeoutMs;
if (earliestTimeoutMs === -1) {
// If no explicit threshold is given, default to an abitrarily large
// value. The actual size doesn't matter because the threshold for the
// whole tree will be clamped to the expiration time.
absoluteTimeoutMs = maxSigned31BitInt;
} else {
if (startTimeMs === -1) {
// This suspend happened outside of any already timed-out
// placeholders. We don't know exactly when the update was
// scheduled, but we can infer an approximate start time from the
// expiration time. First, find the earliest uncommitted expiration
// time in the tree, including work that is suspended. Then subtract
// the offset used to compute an async update's expiration time.
// This will cause high priority (interactive) work to expire
// earlier than necessary, but we can account for this by adjusting
// for the Just Noticeable Difference.
const earliestExpirationTime = findEarliestOutstandingPriorityLevel(
root,
renderExpirationTime,
);
const earliestExpirationTimeMs = expirationTimeToMs(
earliestExpirationTime,
);
startTimeMs = earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
}
absoluteTimeoutMs = startTimeMs + earliestTimeoutMs;
}
// Mark the earliest timeout in the suspended fiber's ancestor path.
// After completing the root, we'll take the largest of all the
// suspended fiber's timeouts and use it to compute a timeout for the
// whole tree.
renderDidSuspend(root, absoluteTimeoutMs, renderExpirationTime);
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime = renderExpirationTime;
return;
}
// This boundary already captured during this render. Continue to the next
// boundary.
workInProgress = workInProgress.return;
} while (workInProgress !== null);
// No boundary was found. Fallthrough to error mode.
value = new Error(
'An update was suspended, but no placeholder UI was provided.',
);
}
// 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 方法 就是设置全局变量 nextRenderDidError 为 true
renderDidError();
// 返回错误调用信息字符串
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
// 根据tag匹配处理程序
do {
// 它其实就是往上去找它,要找到第一个可以处理错误的 class component
// 来进行一个错误的update的一个创建,并且让它入栈
// 等后期在commit的时候可以进行一个调用
// 如果都没有,那么它会到 HostRoot 上面来进行处理错误
// 因为 HostRoot 它相当于是一个内置的错误处理的方式
// 也会创建对应的update,然后进行一个入队列,然后后续进行一个调用的过程
// 这就是一个 throw exception,对于错误处理的一个情况
switch (workInProgress.tag) {
case HostRoot: {
const errorInfo = value;
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime = renderExpirationTime;
// 这个 update 类似于 setState 创建的对象
const update = createRootErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
);
enqueueCapturedUpdate(workInProgress, update);
return;
}
//
case ClassComponent:
// Capture and retry
const errorInfo = value;
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
// 它要先去判断一下,它现在没有 DidCapture 这个 SideEffect
// 并且它是有 getDerivedStateFromError 这么一个方法
// 或者它是有 componentDidCatch 生命周期方法
if (
(workInProgress.effectTag & DidCapture) === NoEffect &&
(typeof ctor.getDerivedStateFromError === 'function' ||
(instance !== null &&
typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
// 在这种情况下,我们就可以给它加上 ShouldCapture 这个 SideEffect
// 并且呢设置它的 expirationTime 等于 renderExpirationTime
// 因为我要去对这个组件, 在这个周期里面进行一个更新的过程
// 然后他也要去创建一个update调用的是 createClassErrorUpdate
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);
}
renderDidError
// packages/react-reconciler/src/ReactFiberScheduler.js
function renderDidError() {
nextRenderDidError = true;
}
createCapturedValue
// packages/react-reconciler/src/ReactCapturedValue.js
export function createCapturedValue<T>(
value: T,
source: Fiber,
): CapturedValue<T> {
// If the value is an error, call this function immediately after it is thrown
// so the stack is accurate.
return {
value,
source,
stack: getStackByFiberInDevAndProd(source),
};
}
getStackByFiberInDevAndProd
function describeFiber(fiber: Fiber): string {
switch (fiber.tag) {
case IndeterminateComponent:
case LazyComponent:
case FunctionComponent:
case ClassComponent:
case HostComponent:
case Mode:
const owner = fiber._debugOwner;
const source = fiber._debugSource;
const name = getComponentName(fiber.type);
let ownerName = null;
if (owner) {
ownerName = getComponentName(owner.type);
}
return describeComponentFrame(name, source, ownerName);
default:
return '';
}
}
// 类似于js里面的error对象,它会有一个stack的信息
// 就是哪个文件或者哪个方法调用的时候,它出现了错误,并且附上文件对应的代码的行数之类的信息
export function getStackByFiberInDevAndProd(workInProgress: Fiber): string {
let info = '';
let node = workInProgress;
// 形成一个错误调用信息的过程
do {
info += describeFiber(node);
node = node.return;
} while (node);
return info;
}
createRootErrorUpdate
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime,
): Update<mixed> {
const update = createUpdate(expirationTime);
// Unmount the root by rendering null.
update.tag = CaptureUpdate;
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element: null};
const error = errorInfo.value;
update.callback = () => {
// 打印 error
onUncaughtError(error);
logError(fiber, errorInfo);
};
return update;
}
enqueueCapturedUpdate
// 没有则创建,有则克隆
// 挂载 update
export function enqueueCapturedUpdate<State>(
workInProgress: Fiber,
update: Update<State>,
) {
// Captured updates go into a separate list, and only on the work-in-
// progress queue.
let workInProgressQueue = workInProgress.updateQueue;
if (workInProgressQueue === null) {
workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(
workInProgress.memoizedState,
);
} else {
// TODO: I put this here rather than createWorkInProgress so that we don't
// clone the queue unnecessarily. There's probably a better way to
// structure this.
workInProgressQueue = ensureWorkInProgressQueueIsAClone(
workInProgress,
workInProgressQueue,
);
}
// Append the update to the end of the list.
if (workInProgressQueue.lastCapturedUpdate === null) {
// This is the first render phase update
workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;
} else {
workInProgressQueue.lastCapturedUpdate.next = update;
workInProgressQueue.lastCapturedUpdate = update;
}
}
createClassErrorUpdate
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime,
): Update<mixed> {
// 创建 update
const update = createUpdate(expirationTime);
// 标记 tag
update.tag = CaptureUpdate;
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
// 存在 getDerivedStateFromError 则作为 payload 回调处理
if (typeof getDerivedStateFromError === 'function') {
const error = errorInfo.value;
update.payload = () => {
return getDerivedStateFromError(error);
};
}
const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
// 这个有组件错误被捕获之后,它会向上去找有能够处理捕获错误信息的这个class component 来处理
// 如果都没有,它才会到 root 上面来进行一个处理
// 它会根据像 getDerivedStateFromError 以及 componentDidCatch 这些生命周期方法来进行一个处理
// 如果都没有这个指定,那么这个classcomponent 是没有一个错误处理的功能的
// 如果有就会对应的进行这些操作来进行一个调用
update.callback = function callback() {
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;
// 输出 error
logError(fiber, errorInfo);
// 调用catch回调钩子 传入 stack
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : '',
});
if (__DEV__) {
if (typeof getDerivedStateFromError !== 'function') {
// If componentDidCatch is the only error boundary method defined,
// then it needs to call setState to recover from errors.
// If no state update is scheduled then the boundary will swallow the error.
warningWithoutStack(
fiber.expirationTime === Sync,
'%s: Error boundaries should implement getDerivedStateFromError(). ' +
'In that method, return a state update to display an error message or fallback UI.',
getComponentName(fiber.type) || 'Unknown',
);
}
}
};
}
return update;
}
经过以上的处理,在调用了所有 exception 之后,最后 立马调用了 completeUnitOfWork
这就说明这个节点报错了,这个节点已经完成了,它不会再继续去渲染它的子节点了
因为这个节点它已经出错了,再渲染它的子节点是没有任何意义
所以在这里面,如果有一个节点,出错了,就会立马对它执行 completeUnitOfWork
它走的就是 unwindWork 的流程了, 这个后续来看