unwindWork
1 )概述
- 在
renderRoot
的 throw Exception 里面, 对于被捕获到错误的组件进行了一些处理 - 并且向上去寻找能够处理这些异常的组件,比如说 class component 里面具有
getDerivedStateFromError
或者componentDidCatch
这样的生命周期方法- 这个class component 就代表它可以处理它的子树当中渲染出来的任何的错误
- 但是在这个过程当中,只是在上面增加了一些 SideEffect
- 比如说, 在出错的那个组件上,增加了 Incomplete
- 而对于能够处理这个错误的组件,增加了 ShouldCapture
- 这些 SideEffect 最终会被如何进行处理, 这时候就要用到 unwindWork 了
- 类似于
completeWork
,对于不同组件, 进行一些不同的处理- 它整个流程是跟 completeWork 完全区分开的
- 它当然也要去做一些 completWork 里面会做的一些工作
- 但是它们肯定会有一些区别,不然它们也不需要进行一个区分
- 对于 ShouldCapture 组件会设置 DidCapture 副作用
2 )源码
定位到 packages/react-reconciler/src/ReactFiberScheduler.js#L1354
定位到 throwException
js
throwException(
root,
returnFiber,
sourceFiber, // 报错的那个组件
thrownValue,
nextRenderExpirationTime,
);
nextUnitOfWork = completeUnitOfWork(sourceFiber);
continue;
-
进入
throwException
下面是精简版,只看结构jsfunction throwException( root: FiberRoot, returnFiber: Fiber, sourceFiber: Fiber, value: mixed, renderExpirationTime: ExpirationTime, ) { // 添加 Incomplete // The source fiber did not complete. sourceFiber.effectTag |= Incomplete; // Its effect list is no longer valid. // 清空 Effect 链 sourceFiber.firstEffect = sourceFiber.lastEffect = null; // ... 其他代码忽略
-
进入
completeUnitOfWork
下面是精简版,只看结构jsfunction completeUnitOfWork(workInProgress: Fiber): Fiber | null { while (true) { // ... 跳过很多代码 // 符合这个条件,走的是 completeWork if ((workInProgress.effectTag & Incomplete) === NoEffect) { // This fiber completed. if (enableProfilerTimer) { // ... 跳过很多代码 nextUnitOfWork = completeWork( current, workInProgress, nextRenderExpirationTime, ); // ... 跳过很多代码 } else { nextUnitOfWork = completeWork( current, workInProgress, nextRenderExpirationTime, ); } // ... 跳过很多代码 } else { // ... 跳过很多代码 // 否则,走的是 unwindWork, 对于不同的组件,这个返回值也会不同 const next = unwindWork(workInProgress, nextRenderExpirationTime); // ... 跳过很多代码 if (next !== null) { // ... 跳过很多代码 next.effectTag &= HostEffectMask; // 注意这个运算,只有在 当前effect和HostEffectMask共有的,才会最终留存下来 return next; // 看到这边 return 了 next } if (returnFiber !== null) { // Mark the parent fiber as incomplete and clear its effect list. returnFiber.firstEffect = returnFiber.lastEffect = null; returnFiber.effectTag |= Incomplete; } } } return null; }
-
在
next.effectTag &= HostEffectMask
这个运算中,这边可能会有一个问题 -
Incomplete 是在 sourceFiber 上面,也就是说报错的那个组件上面的
-
但是它向上寻找的就是能够处理错误的组件, 它增加的是 ShouldCapture
-
需要注意的一点是,一开始进来处理的第一个组件,它是报错的那个组件,它并不一定是能够处理错误的那个组件
-
因为在 unwindWork 里面,增加这个 ShouldCapture 的时候
-
是我们在一个循环中向报错的那个组件的父链上面去寻找,可以处理错误的那个组件
-
一般它是 HostRoot 或者是 ClassComponent
-
所以一进来的时候处理的组件是报错那个组件,它可能不是一个ClassComponent,或者它没有错误处理的能力
-
这个时候它不一定会进来这个 next !== null 的这个判断,那么它会往下走
-
那往下走的时候,它判断了returnfivever不等于诺的情况
-
它会去给 returnFiber 增加这个 Incomplete 的 effectTag
-
也就是说如果一个子树当中的组件报错了,对于它父链上的所有组件的 completeUnitOfWork
-
都会执行对应的 unwindWork 的流程,而不会走 completeWork的流程
-
进入
unwindWork
jsfunction unwindWork( workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) { switch (workInProgress.tag) { // 在这里面,它根据不同的组件类型处理了这些内容 // 它主要处理的就是 ClassComponent, HostComponent SuspenseComponent // 剩下的一些基本上都是跟 completeWork 里面类似 // 对于这些组件,它跟 completeWork 最大的区别就是它会去判断 ShouldCapture 这个 SideEffect // 如果我们这个组件上面有 ShouldCapture 这个 SideEffect // 那么它会把 ShouldCapture 给它去掉, 然后增加这 DidCapture 这个 SideEffect // 这就是对于 classComponent 跟 completeWork 里面的一个最主要的区别 case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { // 注意这里 workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; return workInProgress; } return null; } // 与 ClassComponent 类似 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; } case HostComponent: { popHostContext(workInProgress); return null; } case SuspenseComponent: { const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; // Captured a suspense effect. Set the boundary's `alreadyCaptured` // state to true so we know to render the fallback. const current = workInProgress.alternate; const currentState: SuspenseState | null = current !== null ? current.memoizedState : null; let nextState: SuspenseState | null = workInProgress.memoizedState; if (nextState === null) { // No existing state. Create a new object. nextState = { alreadyCaptured: true, didTimeout: false, timedOutAt: NoWork, }; } else if (currentState === nextState) { // There is an existing state but it's the same as the current tree's. // Clone the object. nextState = { alreadyCaptured: true, didTimeout: nextState.didTimeout, timedOutAt: nextState.timedOutAt, }; } else { // Already have a clone, so it's safe to mutate. nextState.alreadyCaptured = true; } workInProgress.memoizedState = nextState; // Re-render the boundary. return workInProgress; } return null; } case HostPortal: popHostContainer(workInProgress); return null; case ContextProvider: popProvider(workInProgress); return null; default: return null; } }
- 注意,ClassComponent和HostRoot的return内容
- 对于HostRoot来说,所有情况下都 return workInProgress
- 而对于 ClassComponent 在有 ShouldCapture 的时候,return的是当前 workInProgress, 否则是return null
-
-
还是拿出之前的图来说这个整体流程
- 比如说,上面这张图里面 List 这个组件它渲染的时候报错了
- 但是它没有
getDerivedStateFromError
或者componentDidCatch
这样的生命周期方法 - 那么它是不会增加 ShouldCapture 这个 SideEffect 的
- 如果这个时候,App组件具有
getDerivedStateFromError
或者componentDidCatch
- 这个时候 App 增加的是 ShouldCapture 这个 SideEffect
- 在 throw Exception 处理完之后,要执行的
completeUnitOfWork
第一个节点是 List 这个组件- 也就是说它执行的时候,它里面的 next 是 null,会继续往下面的 returnFiber 判断中去
- 得到的 returnFiber 是 div, 对 div 增加了这些 SideEffect 之后
- 它又会走
completeUnitOfWork
之后,仍然走的是unwindWork
- 这时的 div 是一个 HostComponent,它不会处理错误,然后又走到下一个
- 走到 App,到 unwindWork 之后,发现它是有 ShouldCapture 这个 SideEffect 的
- 它 return 的是 workInProgress,之后有next了,这边就可以 return next
- 并且它上面会具有 DidCapture 的 SideEffect
- 它 return next 之后,对于
completeUnitOfWork
相当于是return了一个Fiber对象 - 也就是说 renderRoot 处理异常后面
completeUnitOfWork
返回的 nextUnitOfWork 对象 - 就变成了App组件它对应的Fiber对象,也就是说,nextUnitOfWork 现在等于App
- 我们的 do while 循环仍然要继续,依然继续调用
workLoop
- 调用 workLoop 就会调用
performUnitOfWork
,然后调用beginWork
- 所以对于 App 这个组件,又需要去重新走一遍更新的流程
- 但是走更新的流程的时候,这个时候已经不一样了,不一样在哪里呢?
- App它是一个 ClassComponent,所以我们要走的是
updateClassComponent
- 在 ReactFiberBeginWork.js 中,找到
updateClassComponent
- 这边正常走下来,其实都是差不多的,创建这些流程之类的
- 这边需要注意的是,比如说
updateClassInstance
- 它对应的是在 ReactFiberClassComponent.js 里面,找到这个方法
- 在这个方法里面,它会去
processUpdateQueue
- 在按 unwindWork的时候,给ClassComponent 创建了一个update 叫
createClassErrorUpdate
- 这个 update 它会去调用
getDerivedStateFromError
,以及它会有一个callback是调用 componentDidCatch - 所以如果有
getDerivedStateFromError
这个方法,对应的在 ClassComponent 里面 - 在进行
processUpdateQueue
的时候,肯定会调用这个方法 - 它就会计算出有错误的情况下它的一个state
- 这个state就会引导 ClassComponent 去渲染错误相关的UI
- 这就是我们的组件 ClassComponent 去捕获错误,并且去渲染出错误相关UI的一个流程
- 因为渲染的是错误相关的UI, 所以原先的它的子树肯定是不会再被渲染出来的
- 或者有可能会被渲染出来, 这个情况视具体的内容而定
- App它是一个 ClassComponent,所以我们要走的是
- 在这种情况下,再回到
beginWork
,再往下调用finishClassComponent
的时候 - 有一个判断是
if ( didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {}
didCaptureError
来自于我们这个组件上面是否有 DidCapture 这个 SideEffect- 在有错误的情况下,它这个属性肯定是 true
- 并且没有 getDerivedStateFromError 的情况,先不管,继续往下看
- 下面的一个判断
if (current !== null && didCaptureError) {}
-
它会执行的一个方法是
forceUnmountCurrentAndReconcile
-
这个情况就是最常见的一个情况,就是组件是在一个更新的过程当中
-
然后它的子树出现了错误,这个App要去渲染错误,它这边的 didCaptureError 就是 true
jsfunction 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. workInProgress.child = reconcileChildFibers( workInProgress, current.child, 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. workInProgress.child = reconcileChildFibers( workInProgress, null, nextChildren, renderExpirationTime, ); }
-
1 )它先调用了一遍 reconcileChildFibers, 先看下它的构造结构
jsfunction reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, expirationTime: ExpirationTime, ): Fiber | null {} ``` * 这边需要注意的是, 它传入的 newChild 是 null * 就是说它要强制把目前所有的子树的节点全部给它删掉 * 它渲染出没有子树的 ClassComponent
-
2 )然后再渲染一遍
- 这个时候传入的 currentFirstChild (老的 children) 是 null
- 然后传入的 newChild 是 nextChildren
- 因为这个 nextchildren 是我们已经从新的(有错误的)update里面, 计算出的一个新的 state
- 这个children, 一般来说,它跟老的 children 是完全不一样的
- 就算不是完全不一样,也可能是大部分不一样
- 它强制在第一次的时候直接用 null, 把它的子树给清空,然后渲染新的 children
- 这样的效率会更高一点,就不需要通过key去对比这些流程了
-
-
- 这就是在react 它的 error boundary 这个功能
- 提供我们 ClassComponent , 使用
getDerivedStateFromError
或者componentDidCatch
- 这样的生命周期方法来处理,捕获到错误之后的一个流程
- 提供我们 ClassComponent , 使用
- 同样的, 这是
unwindWork
处理流程当中需要注意的一些点