前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码解析之「错误处理」流程

React源码解析之「错误处理」流程

作者头像
进击的小进进
发布2020-04-01 15:13:59
9150
发布2020-04-01 15:13:59
举报
前言

React源码解析之renderRoot概览 中提到了,当有异常抛出的时候,会执行completeUnitOfWork()

      //捕获异常,并处理
      catch (thrownValue)
      {
        //抛出可预期的错误
        throwException(
          root,
          returnFiber,
          sourceFiber,
          thrownValue,
          renderExpirationTime,
        );
        //完成对sourceFiber的渲染,
        //但是因为已经是报错的,所以不会再渲染sourceFiber的子节点了
        //sourceFiber 即报错的节点
        workInProgress = completeUnitOfWork(sourceFiber);
      }

注意:throwException()中,会对报错的fiber添加IncompleteeffectTag

  // The source fiber did not complete.
  //effectTag 置为 Incomplete
  //判断节点更新的过程中出现异常
  sourceFiber.effectTag |= Incomplete;

本篇文章就来解析 React 是如何捕获并处理错误的

一、completeUnitOfWork

(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;
      }
    }

显然,effectTagIncomplete进行逻辑与后,是不可能等于NoEffect的,所以会执行else{ }的情况

(2) else情况中,先执行unwindWork,如果返回的 next 不为null的话,则执行

next.effectTag &= HostEffectMask

除去IncompleteShouldCaptureeffectTag,而保留DidCaptureeffectTag,为什么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

二、unwindWork

作用: 根据不同组件的类型和目标节点的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)重点看下ClassComponentHostComponent的情况: ① fiber对象的tagHostComponent的话,那么该 fiber是 DOM 标签元素(div、span...),并且直接 return null 了。

返回null的意思是,当前节点不具备处理错误的能力,只能交由父节点去处理,一直往上,直到找到能处理错误的节点,比如ClassComponent

ClassComponent是能够处理 error 的,它对 fiber 节点进行的操作是: 去掉ShouldCapture,加上DidCaptureeffectTag,这表示捕获到 error 了,然后返回该 fiber 节点

联系一、completeUnitOfWork可知: (1) throwException()为报错的fiber添加IncompleteeffectTag (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()关于错误捕获的源码

三、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()

四、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(),更新时,将内部节点全部删除,目的是不渲染项目页面

此时会catchthrownValue,那么就会返回到「前言」所说的源码上,再次执行throwException(),让ClassComponent渲染出捕获 error 的 ui 页面

补充: 关于reconcileChildFibers(),请看: React源码解析之FunctionComponent(上)

最后

比较绕,逻辑是: 当有一个节点 throwError 后,给该节点一个Incomplete的 effectTag,但只有ClassComponent能捕获错误,所以会一层层向上找ClassComponent,并给每个父级添加Incomplete的 effectTag,直到找到ClassComponent后,清空它的子节点(也就是不渲染出项目页面),并再次 throwError,此时React 会调用throwException(),对ClassComponent节点进行处理,逐层渲染出catch error的 ui 页面。

GitHub

ReactFiberUnwindWork.js

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberUnwindWork.js

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、completeUnitOfWork
  • 二、unwindWork
  • 三、finishClassComponent
  • 四、forceUnmountCurrentAndReconcile
  • 最后
  • GitHub
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档