React 错误边界(Error Boundary)处理逻辑:解析从捕捉错误到调度降级 Fiber 树的执行流

嘿,各位前端开发者,欢迎来到今天的“React 深度解剖实验室”。我是你们的领航员,今天我们要聊的东西,有点硬核,有点带劲,甚至可能让你头皮发麻——但绝对会让你大呼过瘾。

今天的话题是:React 错误边界(Error Boundary)处理逻辑:解析从捕捉错误到调度降级 Fiber 树的执行流。

别被这个标题吓到了。我知道,当你看到“执行流”、“Fiber 树”、“调度”这些词的时候,你的脑子里是不是已经开始播放那种像是在深海里潜水、周围全是气泡和代码块的BGM了?是不是觉得“这玩意儿我平时写代码用不到,所以我就不用懂”?

停!打住!

这是大忌。就像你开法拉利(React),却不知道引擎盖下面那堆精密的齿轮(Fiber)是怎么咬合的一样,你永远只能做个只会调包的“代码搬运工”。要想成为那种“别人报错我微笑,别人踩坑我飞升”的资深专家,你必须得把 React 的内脏掏出来看看。

今天,我们不整那些虚头巴脑的“React 18 带来了什么新特性”的废话,我们直接钻进 React 的核心,去看看当你的 App 崩溃的时候,到底发生了什么。我们不看表面,我们要看那一层层被剥开后的真相。

准备好了吗?系好安全带,我们要开始下潜了。

第一章:为什么 try-catch 在 React 里是个骗子?

在开始讲 Fiber 之前,我们必须先解决一个“信仰崩塌”的问题。

你可能在很多教程里见过这样的写法:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  // 这里的 catch 逻辑,是不是看起来很顺眼?
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 上报错误...
  }

  render() {
    if (this.state.hasError) {
      return <h1>哎呀,出错了!</h1>;
    }
    return this.props.children;
  }
}

// 使用:
<ErrorBoundary>
  <MyComponent /> {/* 这里可能会报错 */}
</ErrorBoundary>

然后你可能会想:“嘿,这不就是 try-catch 吗?把组件包起来,有错就捕获,没报错就渲染子组件,这逻辑不简单吗?”

如果你这么想,那你就在“自欺欺人”了。

try-catch 在 React 的渲染阶段是失效的。

为什么?因为 React 的渲染是同步的(虽然在 React 18 引入了并发模式,但渲染阶段的本质依然是同步执行逻辑,只是被中断了而已)。try-catch 捕获的是同步代码中的异常。但 React 在渲染时,它抛出的错误,是经过 React 内部封装的“魔法抛掷”。当你写一个函数组件或者类组件时,你是在写普通的 JavaScript 代码。如果这段代码抛出了异常,React 会怎么处理?

React 会把它当成一个致命错误,直接把它扔出 React 的渲染循环,然后触发错误边界。

重点来了: ErrorBoundary 里的 getDerivedStateFromErrorcomponentDidCatch,它们并不是在 try-catch 块里被调用的。它们是 React 内部机制触发的回调。React 就像一个严厉的监工,它看着你干活,如果你突然晕倒(报错),监工不会把你扶起来(try-catch),监工会直接冲过来,大喊一声:“这活儿干不了了!启动降级方案!”

所以,Error Boundary 的本质,不是代码层面的异常捕获,而是架构层面的状态降级

第二章:Fiber 树—— React 的瑞士奶酪模型

要理解“降级”,首先得理解 React 的数据结构:Fiber 树

你可以把 React 的渲染过程想象成一支正在搭建乐高城堡的施工队。这支施工队不是一个人在干,而是一群人,每个人(每个 Fiber 节点)负责一块区域。

Fiber 树不仅仅是一棵树,它是一个链表。每个节点都是一个对象,长得像这样(概念版):

function FiberNode(type, props, key, mode) {
  // 1. 身份信息:我是谁?
  this.type = type; // 比如 'div', 'Button'
  this.key = key;
  this.tag = ...; // 节点类型标签

  // 2. 状态管理:我现在的状态是什么?
  this.stateNode = null; // DOM 节点、Class 实例等
  this.memoizedState = null; // Hooks 状态
  this.updateQueue = null; // 待更新的队列

  // 3. 指针:我的邻居是谁?
  this.return = null; // 父节点
  this.child = null;  // 第一个子节点
  this.sibling = null; // 下一个兄弟节点

  // 4. 标志位:我干了什么?(这才是重点!)
  this.flags = ...; // 标记了是新增、删除还是更新
}

React 的渲染过程,就是在这个链表结构上进行的深度优先遍历

第三章:渲染阶段的“挖掘”之旅

当你的点击事件触发,或者父组件更新时,React 会进入渲染阶段。这个阶段的主要任务是:

  1. 遍历 Fiber 树。
  2. 根据当前的 Props 和 State,计算出“新状态”是什么。
  3. 检查新旧状态是否一致。

这就像施工队在数积木,算算看现在的城堡跟上次建的一样不一样。

假设我们的 Fiber 树结构是这样的:

// Root Fiber (根节点)
RootFiber
  ├── Child Fiber (ErrorBoundary)
  │     ├── Child Fiber (ChildComponent)
  │     └── ...
  └── ...

React 会从 RootFiber 开始,调用 beginWork 函数。

// 简化的 beginWork 逻辑
function beginWork(currentFiber) {
  // 1. 如果是 ErrorBoundary,检查它的状态
  if (currentFiber.tag === ClassComponent) {
    const instance = currentFiber.stateNode;
    // 如果之前已经捕获过错误了,就别渲染子组件了,直接渲染回退 UI
    if (instance._errorCaptured) {
      return <FallbackUI />; // 返回降级 UI
    }
  }

  // 2. 如果是普通子组件,比如 ChildComponent
  // React 会调用 render 函数
  try {
    const childFiber = createFiberFromTypeAndProps(currentFiber.type, ...);
    childFiber.return = currentFiber; // 建立父子关系
    currentFiber.child = childFiber;
  } catch (error) {
    // 3. 噢!出错了!
    // 注意:这里没有 try-catch,是 React 内部捕获的
    captureError(currentFiber, error);
  }
}

关键点来了:ChildComponentrender 函数里,如果你写了一行 throw new Error("Boom!")

React 怎么知道?React 就在 createFiberFromTypeAndProps 里面,或者在你调用组件函数的时候,直接捕获了这个 throw

第四章:捕捉错误—— 向上冒泡

当错误被抛出时,React 不会傻傻地让错误乱飞。React 内部有一个错误捕获机制。它会把这个错误信息附加到当前正在渲染的 Fiber 节点上。

这个 Fiber 节点是谁?就是报错的那个 ChildComponent 对应的节点。

然后,React 会停止在这个节点上的工作,开始向上回溯

// 简化的错误捕获逻辑
function captureError(fiber, error) {
  // 1. 设置当前节点的错误状态
  fiber.flags |= Placement; // 标记需要插入(或者说是需要更新)
  fiber.updateQueue = { error }; // 把错误塞进更新队列

  // 2. 向上找爹
  let node = fiber.return;
  while (node) {
    // 3. 如果找到了 ErrorBoundary
    if (node.tag === ClassComponent && node.stateNode) {
      // 4. 调用 ErrorBoundary 的生命周期!
      // getDerivedStateFromError 在这里被调用
      const errorInfo = {
        componentStack: getComponentStack(fiber),
        error: error,
      };

      // 这里是魔法发生的地方:
      // React 修改 ErrorBoundary 的 state
      const nextState = ErrorBoundary.getDerivedStateFromError(error);
      if (nextState !== null) {
        node.stateNode._errorCaptured = true; // 标记已捕获
        node.stateNode.setState(nextState);   // 触发状态更新
      }

      // componentDidCatch 会在稍后的提交阶段调用
      ErrorBoundary.componentDidCatch(error, errorInfo);

      return; // 停止向上冒泡,任务完成
    }
    node = node.return;
  }

  // 如果一直找不到 ErrorBoundary,那就完了,应用崩溃!
}

这一步非常关键。你看,getDerivedStateFromError 被调用,它返回一个新的状态(通常是 { hasError: true })。这个状态更新,会触发 React 重新渲染整个组件树。

第五章:调度降级 Fiber 树—— 这里的风景不一样

好了,现在我们回到了 beginWork。此时,React 已经拿到了 ErrorBoundary 的最新状态(hasError: true)。

React 再次遍历 Fiber 树。这一次,当它走到 ErrorBoundary 这个节点时,逻辑完全变了。

这就是“降级”的真正含义。

在正常的渲染流程中,beginWork 会调用组件的 render 方法,生成子 Fiber 节点。
但在降级流程中,React 检测到 ErrorBoundary 已经进入错误状态。

function beginWork(currentFiber) {
  // 检查:我是 ErrorBoundary 吗?
  if (currentFiber.tag === ClassComponent) {
    const instance = currentFiber.stateNode;

    // 逻辑分支 A:正常模式
    if (!instance._errorCaptured) {
      // 正常渲染子组件
      const childFiber = createFiberFromTypeAndProps(currentFiber.type, ...);
      return childFiber;
    }

    // 逻辑分支 B:降级模式
    // 注意!这里没有调用 currentFiber.type.render()
    // React 直接返回了降级 UI 的 Fiber 节点!
    const fallbackFiber = createFiberFromElement(<FallbackUI />);

    // 关键点:降级模式下,这个 ErrorBoundary 的子节点链表被切断了
    // 它不再负责渲染原本的 ChildComponent 了
    return fallbackFiber;
  }
}

执行流的变化:

  1. 正常树: Root -> ErrorBoundary -> ChildComponent -> ...
  2. 降级树: Root -> ErrorBoundary -> FallbackUI

你看,Fiber 树的结构变了。原本指向 ChildComponent 的指针(child),现在指向了 FallbackUI。原本属于 ChildComponent 的那一整片子树,在 React 的眼中被“丢弃”了。

这就是所谓的“降级 Fiber 树”。React 构建的是一棵新的树,这棵树不再包含报错的逻辑,而是包含了一个静态的、安全的回退 UI。

第六章:提交阶段—— 把降级树画到屏幕上

现在,渲染阶段结束了。React 有了两棵树:

  1. WorkInProgress 树(降级树): 刚刚计算出来的,包含 FallbackUI 的树。
  2. Current 树(旧树): 屏幕上现在显示的树。

React 进入提交阶段。这个阶段是同步的,它会直接操作 DOM。

function commitRoot(root) {
  // 1. 遍历 WorkInProgress 树
  const nextEffect = root.firstEffect;

  while (nextEffect) {
    // 2. 执行副作用
    if (nextEffect.effectTag === Placement) {
      commitPlacement(nextEffect); // 插入 DOM
    }

    // 3. 调用 componentDidCatch
    if (nextEffect.effectTag === Callback) {
      commitCallbacks(nextEffect);
    }

    // 4. 完成这一帧的工作
    nextEffect = nextEffect.nextEffect;
  }

  // 5. 树的切换
  root.current = root.workInProgress;
}

当 DOM 被更新,屏幕上原本复杂的页面瞬间变成了一个简单的“哎呀,出错了!”的提示框。这就是降级 Fiber 树执行完毕后的结果。

第七章:深入细节—— 并发模式下的降级

如果你在 React 18 的环境下看代码,你会发现事情变得更复杂了。因为有了并发模式,React 可以在渲染中途打断你的工作。

想象一下,ErrorBoundary 的渲染过程非常耗时,或者它的子组件 ChildComponent 正在执行一个复杂的计算,突然报错了。

在 React 18 之前,这就像是一辆火车,一旦开始跑,就不能停下,必须开完全程,直到撞车。React 会捕获错误,触发降级,然后完成渲染。

但在 React 18(并发模式)下,事情是这样的:

  1. 中断: React 在渲染 ChildComponent 的时候,发现这事儿太慢了,或者有一个更高优先级的任务(比如用户点击了另一个按钮)。React 会暂停当前的渲染工作。
  2. 错误捕获: 在暂停的间隙,ChildComponent 抛出了错误。React 捕获了这个错误。
  3. 重新调度: React 决定:算了,这棵树没法建了,我要换一棵树(降级树)。
  4. 恢复: React 重新启动渲染过程,这次它构建的是降级 Fiber 树。
  5. 取消: 原本那个还在慢吞吞执行的 ChildComponent 的任务,会被标记为取消,直接扔进垃圾回收站。

这就像是你正在盖房子(渲染),突然发现地基塌了(报错),这时候你不仅不能继续盖,还得把刚才盖了一半的墙拆了(取消工作),然后开始用预制板盖个临时棚子(降级树)。

第八章:代码示例—— 完整的执行流模拟

为了让你彻底明白,我们来写一段伪代码,模拟整个流程。

// 1. 用户点击按钮
function handleClick() {
  // React 调度器开始工作
  scheduleRoot();
}

// 2. React 开始渲染阶段
function renderRoot() {
  let nextUnitOfWork = workInProgressRoot.nextUnitOfWork;

  while (nextUnitOfWork) {
    // 尝试完成一个工作单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
}

// performUnitOfWork 的核心逻辑
function performUnitOfWork(fiber) {
  // 如果是 ErrorBoundary,检查是否需要降级
  if (fiber.tag === ErrorBoundary) {
    if (fiber.stateNode._errorCaptured) {
      // 降级模式:不渲染子组件,渲染 Fallback
      return createFiberElement(FallbackComponent);
    }
  }

  // 如果是普通组件
  try {
    // 尝试渲染组件
    const children = fiber.type.render(fiber.props);
    // 创建子 Fiber
    const childFiber = createFiber(children);
    fiber.child = childFiber;
    return childFiber; // 继续往下走
  } catch (error) {
    // 3. 捕捉到错误!
    // 修改当前 Fiber 的状态
    fiber.updateQueue = { error };

    // 向上冒泡
    let node = fiber.return;
    while (node) {
      if (node.tag === ErrorBoundary) {
        // 4. 调用 getDerivedStateFromError
        const nextState = node.type.getDerivedStateFromError(error);
        if (nextState) {
          node.stateNode.setState(nextState);
          node.stateNode._errorCaptured = true;
        }
        // 5. 调用 componentDidCatch
        node.type.componentDidCatch(error, { componentStack: ... });
        return null; // 停止当前节点的工作
      }
      node = node.return;
    }
    throw error; // 没找到边界,抛出给 React Core
  }
}

// 3. 提交阶段
function commitRoot() {
  // 遍历 WorkInProgress 树,把 DOM 插入页面
  let fiber = workInProgressRoot.firstEffect;
  while (fiber) {
    if (fiber.effectTag === Placement) {
      commitPlacement(fiber);
    }
    fiber = fiber.nextEffect;
  }
  // 切换 Current 树
  currentRoot = workInProgressRoot;
  isWorking = false;
}

第九章:关于 Fiber 树的“副作用”

你可能会问:“那个 FallbackUI 是怎么生成 Fiber 节点的?”

在 React 中,无论是正常的 JSX 还是降级 JSX,最终都会被 React.createElement 或者 JSX 编译器转换成 Fiber 节点

// 普通组件
const Child = <ChildComponent />;

// 降级组件
const Fallback = <h1>出错了</h1>;

当 React 遇到 <h1>出错了</h1> 时,它会调用 createFiberFromElement。这会生成一个类型为 HostComponent(标签为 ‘h1’)的 Fiber 节点。

所以,降级 Fiber 树并不是凭空变出来的,它是 React 根据错误状态,重新构建的一棵新的树。

第十章:为什么 Error Boundary 只能捕获 Class 组件?

这是一个经典面试题,也是理解 Fiber 标签的关键。

React 在 beginWork 里判断 fiber.tag

switch (fiber.tag) {
  case FunctionComponent:
    // 函数组件没有 stateNode,没有实例
    // render 函数直接执行
    return fiber.type.render(props);

  case ClassComponent:
    // Class 组件有 stateNode(实例),可以挂载 _errorCaptured 标记
    // 可以调用 getDerivedStateFromError
    return fiber.type.render(props);

  case HostComponent:
    // DOM 节点,没有生命周期
    return null;
}

对于函数组件,React 没有实例对象来存储 _errorCaptured 标记,也没有 this 来调用 getDerivedStateFromError。所以,React 只能对 Class 组件做这种“降级”处理。

如果你想在函数组件里做错误处理,只能用 try-catch 包裹组件函数本身,但这会破坏 React 的渲染机制(如上文所述),所以不推荐。

第十一章:终极思考—— 错误边界与 Fiber 的哲学

当我们把 Error Boundary 的逻辑剥开,看到的是 React 对“不可预测性”的优雅处理。

React 假设整个应用是一个巨大的状态机。正常情况下,它从状态 A 渲染到状态 B。如果过程中某个状态(子组件渲染)抛出了异常,React 就不强行把状态 B 渲染出来,而是降级到一个更安全的状态 C(显示错误提示)。

降级 Fiber 树不仅仅是一个技术名词,它是一种架构思维:

  1. 隔离: 错误被限制在特定的 Fiber 节点范围内。
  2. 恢复: 通过状态更新,UI 从错误状态切换到回退状态。
  3. 隔离性: 即使 ErrorBoundary 内部崩了,外部的 Root 依然可以渲染其他内容(虽然 Root 可能也会崩,但至少逻辑上被隔离了)。

结语:去驾驭你的代码

好了,各位,今天的深度解剖就到这里。

我们不再是看着那一层薄薄的 componentDidCatch 就满足的工程师了。我们看到了它背后的 Fiber 链表,看到了 beginWork 的递归,看到了 captureError 的向上冒泡,看到了从正常树到降级树的惊险一跃。

React 的强大,不在于它提供了多少 API,而在于它如何利用 Fiber 架构来管理这种复杂的“状态流转”。

下次当你看到屏幕上那个红色的错误页面时,不要只觉得那是系统 Bug。你要在脑海里构建出那个 Fiber 节点,看到它举起手来,大喊一声“我搞不定了”,然后 React 递归地向上寻找边界,最终切换到那棵降级树。

这就是 React,这就是代码的艺术。

现在,拿起你的编辑器,去优化你的边界吧!记住,最好的错误处理,是永远不报错;但如果你不得不报错,至少要让报错的过程看起来像是一场精心编排的手术,而不是一场混乱的爆炸。

下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注