renderRoot的错误处理
1 )概述
- 在
completeWork这个方法之后, 再次回到renderRoot里面 - 在
renderRoot里面执行了workLoop, 之后,对workLoop使用了try catch - 如果在里面有任何一个节点在更新的过程当中 throw Error 都会被catch到
- catch到之后就是错误处理
- 给报错节点增加 incomplete 副作用
- incomplete 的副作用在
completeUnitOfWork的时候,用来进行判断 - 是要调用 completeWork,还是调用 unwindWork
- incomplete 的副作用在
- 需要给父链上具有 error boundary 的节点增加副作用
- 让它去收集错误以及进行一定的处理
- 还需要创建错误相关的更新
- 给报错节点增加 incomplete 副作用
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);
-
进入
onUncaughtErrorfunction 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), }; }- 进入
getStackByFiberInDevAndProdfunction 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; } - 进入
createRootErrorUpdatefunction 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; } } - 进入
createClassErrorUpdatefunction 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 的流程了, 这个后续来看
663



被折叠的 条评论
为什么被折叠?



