专栏首页前端干货和生活感悟React源码解析之「错误处理」流程

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

前言

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

本文分享自微信公众号 - webchen(webchen1995),作者:webchen

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React源码解析之FunctionComponent(上)

    在 React源码解析之workLoop 中讲到当workInProgress.tag为FunctionComponent时,会进行FunctionCompon...

    进击的小进进
  • React源码解析之FunctionComponent(中)

    作用: 当子节点不为 null,则复用子节点并删除其兄弟节点; 当子节点为 null,则创建新的 fiber 节点

    进击的小进进
  • React源码解析之completeUnitOfWork

    (1) 关于completeUnitOfWork()在哪里使用到,请看下 React源码解析之workLoop 中的二、performUnitOfWork

    进击的小进进
  • Neo4j-1.Neo4j基础

    悠扬前奏
  • PostgreSQL的几种分布式架构对比

    Citus以插件的方式扩展到postgresql中,独立于postgresql内核,所以能很快的跟上pg主版本的更新,部署也比较简单,是现在非常流行的分布式方案...

    数据库架构之美
  • Redis源码学习之跳表

    跳跃链表简称为跳表(SkipList),它维护了一个多层级的链表,且第i+1层链表中的节点是第i层链表中的节点的子集。跳表作为一种平衡数据结构,经常和平衡树进行...

    里奥搬砖
  • Redis主从复制的原理

    在Redis集群中,让若干个Redis服务器去复制另一个Redis服务器,我们定义被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为...

    全菜工程师小辉
  • HTML DOM 学习

    DOM简单来说就是文档对象模型,当一个HTML页面被加载就会创建HTML页面的DOM

    Mirror王宇阳
  • 从概念到实践,我们该如何构建自动微分库

    选自Medium 作者:Maciej Kula 机器之心编译 参与:程耀彤、蒋思源 像 PyTorch 或 TensorFlow 这样通用的自动微分框架是非常有...

    企鹅号小编
  • 3-Kubernetes进阶架构学习操作与配置

    Q:什么是节点? 答:Kubernetes中节点(node)指的是一个工作机器曾经叫做 minion , 但是需要注意不同的集群中,节点可能是虚拟机也可能是物理...

    WeiyiGeek

扫码关注云+社区

领取腾讯云代金券