React 白屏机制原理分析[共1500字,阅读时长8min]

白屏是什么

白屏是React主动防御机制,React团队认为,显示错误的UI比不显示任何内容更糟糕(例如,转账金额显示错误),因此React遇到无法处理的异常,会选择卸载掉整个组件树。对比着看,原生直接操作DOM的方式通常不会造成白屏的现象,而是会停留在错误发生前的状态。

React渲染机制

白屏的出现与React渲染机制息息相关,白屏实际就是:React渲染过程中发生了未捕获错误,React内部错误处理机制,捕获了该错误,然后进行组件卸载,防止显示错误UI。下图是React的渲染流程:

主要可以分为两个阶段:

Render阶段

根据 JSX转换的ReactElement节点 +旧的Fiber节点树 构建出新的Fiber节点树(Fiber可以被看作为是虚拟DOM)。

Commit阶段

根据Render阶段得出的新的Fiber树更新DOM。

渲染触发时机

  1. 组件初始化。
  2. 通过setState触发组件状态更新。

组件初始化状态变更都会走这套渲染流程,只不过会根据Mount阶段/Update阶段的不同,一些处理逻辑不同,整体流程都是一样的。

白屏机制

Render阶段

在源码中,可以把workLoopSync 函数看作是React的Render 阶段,workLoopSync是被try/catch包裹的。一旦workLoopSync过程中遇到不可处理且未捕获的错误 ,如a是undefined,代码中读取a.b.c,这会导致空指针错误,并且这个错误会被try/catch所捕获,进入handleError 函数中。在handleError函数中会触发React的白屏逻辑和Errorboundary逻辑。

总结:当 Render 阶段(如 workLoopSync 构建 Fiber 树)发生未捕获的 JavaScript 错误时,React 会利用内部的 try/catch 捕获异常。为了防止渲染出数据不一致或损坏的 UI,React 会选择卸载整个组件树(即白屏)。

commit阶段

虽然Render 阶段调用的是handleErrorCommit 阶段调用的是captureCommitPhaseError,但是两个函数在处理白屏逻辑是相似的,可以简单认为是一样的。

commit阶段的流程包括:更新DOM,执行useLayoutEffect,执行useEffect。这三部分都被React内置的try/catch 包裹,异常处理函数是captureCommitPhaseError 。如果Commit阶段,代码抛出了未捕获的错误,仍然会触发captureCommitPhaseError函数。

白屏例子

第一个例子

最典型的白屏例子:

javascript 复制代码
import React, { useState } from 'react';

export default function Page404() {
  const [data, setData] = useState(null)
  return <div className="fx-center mt-80">{data.message}</div>;
}

上述代码的执行流程是:

  1. 编译时 :JSX 转变为React.createElement/_jsx,创建ReactElement的函数语句.
  2. 运行时 :组件 Render 过程中, _jsx 函数读取参数,因 data.message 求值失败抛出 TypeError。React 捕获这个错误后,触发了全量的 Unmount 保护机制 ,导致白屏。如果存在ErrorBoundary组件,则会使用Error Boundary兜底。
第二个例子
javascript 复制代码
export default function Page404() {
  const [data, setData] = useState(null)

  console.log(data.a.b)

  return <div className="fx-center mt-80">{data || null}</div>;
}

这段代码同样也会发生白屏,本质上, 函数组件的函数体执行本身 就是 Render 阶段的一部分 。React 在构建 Fiber 树时会直接调用该组件函数。因此, console.log(data.a.b) 抛出的空指针异常,会直接被 React 内部的try/catch捕获,进入handleError 函数,React 卸载整个组件树,从而导致白屏

第二个例子改进
javascript 复制代码
import React, { useState } from 'react';

export default function Page404() {
  const [data, setData] = useState(null)

  try {
    console.log(data.a.b)
  } catch (error) {

  }

  return <div className="fx-center mt-80">{data || '404'}</div>;
}

console.log(data.a.b) 虽然发生了错误,但因为使用了 try/catch ,这个错误没有抛出给 React内部的try/catch,所以 React 并不知情。

第三个例子(这个例子很重要)

点击按钮的时候,是否会发生白屏错误?

javascript 复制代码
import React, { useState } from 'react';

export default function Page404() {
  const [data, setData] = useState(null)

  const handleClick = () => {
    console.log(data.a.b.c)
  }

  return <div className="fx-center mt-80"><button onClick={handleClick}>404</button><h1>{data || '404'}</h1></div>;
}

答案是:不会

当用户点击按钮时, Render 阶段早已结束 ,Commit 阶段也结束了,UI 已经稳定地显示在屏幕上。之前我们说过Render过程的未捕获错误,才会被React内部的try/catch所捕获。

总结: 这个例子表明不是发生了JS错误,就会白屏。白屏对发生时机有着严格要求,必须是渲染过程中,项目才会白屏。

第四个例子

点击按钮的时候,是否会发生白屏错误?

javascript 复制代码
import React, { useState } from 'react';

export default function Page404() {
  const [data, setData] = useState({message:"404"})

  const handleClick = () => {
    setData(null)
  }

  return <div className="fx-center mt-80"><button onClick={handleClick}>404</button><h1>{data.message}</h1></div>;
}

答案是:会 当用户点击按钮时,调用handleClick函数,执行 setData(null) 修改状态,这会触发当前组件重新渲染。在Render过程中,因为data被更新为null,代码执行到 《h1》 {data.message}《/h1》(掘金用h1标签就自动变大了,因此用《》代替<>),因为data是null,代码会抛出空指针报错,这会被React内部的try/catch所捕获,从而触发白屏逻辑。

第五个例子
javascript 复制代码
import React, { useEffect, useState } from 'react';

export default function Page404() {
  const [data, setData] = useState(null)

  useEffect(() => {
    console.log(data.a.b.c)
  }, [])

  return <div className="fx-center mt-80">404</div>;
}

答案是:会白屏 虽然 useEffect 是异步执行的,但它是由 React Scheduler(调度器) 接管的。当 Commit 阶段完成后,Scheduler 会调度并执行 flushPassiveEffects 方法来处理所有副作用。

在此过程中,React 会显式地用 try-catch 包裹回调函数的执行。因此, data.a.b.c 抛出的空指针异常会被 React 精确捕获,并触发 captureCommitPhaseError 流程。

总结

白屏是组件渲染过程中抛出未捕获的错误,被React内部的错误处理机制捕获,从而触发的一种主动防御的机制。

相关推荐
VT.馒头22 分钟前
【力扣】2695. 包装数组
前端·javascript·算法·leetcode·职场和发展·typescript
css趣多多33 分钟前
一个UI内置组件el-scrollbar
前端·javascript·vue.js
-凌凌漆-41 分钟前
【vue】pinia中的值使用 v-model绑定出现[object Object]
javascript·vue.js·ecmascript
源代码•宸1 小时前
大厂技术岗面试之谈薪资
经验分享·后端·面试·职场和发展·golang·大厂·职级水平的薪资
C澒1 小时前
前端整洁架构(Clean Architecture)实战解析:从理论到 Todo 项目落地
前端·架构·系统架构·前端框架
C澒1 小时前
Remesh 框架详解:基于 CQRS 的前端领域驱动设计方案
前端·架构·前端框架·状态模式
Charlie_lll1 小时前
学习Three.js–雪花
前端·three.js
onebyte8bits1 小时前
前端国际化(i18n)体系设计与工程化落地
前端·国际化·i18n·工程化
马猴烧酒.1 小时前
【面试八股|JVM虚拟机】JVM虚拟机常考面试题详解
jvm·面试·职场和发展
C澒1 小时前
前端分层架构实战:DDD 与 Clean Architecture 在大型业务系统中的落地路径与项目实践
前端·架构·系统架构·前端框架