React16源码: React中的unwindWork的源码实现

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 下面是精简版,只看结构

    js 复制代码
    function 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 下面是精简版,只看结构

    js 复制代码
    function 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

      js 复制代码
      function 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, 所以原先的它的子树肯定是不会再被渲染出来的
    • 或者有可能会被渲染出来, 这个情况视具体的内容而定
  • 在这种情况下,再回到 beginWork,再往下调用 finishClassComponent 的时候
  • 有一个判断是 if ( didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {}
    • didCaptureError 来自于我们这个组件上面是否有 DidCapture 这个 SideEffect
    • 在有错误的情况下,它这个属性肯定是 true
    • 并且没有 getDerivedStateFromError 的情况,先不管,继续往下看
  • 下面的一个判断 if (current !== null && didCaptureError) {}
    • 它会执行的一个方法是 forceUnmountCurrentAndReconcile

    • 这个情况就是最常见的一个情况,就是组件是在一个更新的过程当中

    • 然后它的子树出现了错误,这个App要去渲染错误,它这边的 didCaptureError 就是 true

      js 复制代码
      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.
        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, 先看下它的构造结构

        js 复制代码
         function 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
    • 这样的生命周期方法来处理,捕获到错误之后的一个流程
  • 同样的, 这是 unwindWork 处理流程当中需要注意的一些点
相关推荐
passerby606116 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了16 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅16 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅16 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅17 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment17 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅17 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊17 小时前
jwt介绍
前端
爱敲代码的小鱼17 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte18 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc