白屏是什么
白屏是React主动防御机制,React团队认为,显示错误的UI比不显示任何内容更糟糕(例如,转账金额显示错误),因此React遇到无法处理的异常,会选择卸载掉整个组件树。对比着看,原生直接操作DOM的方式通常不会造成白屏的现象,而是会停留在错误发生前的状态。
React渲染机制
白屏的出现与React渲染机制息息相关,白屏实际就是:React渲染过程中发生了未捕获错误,React内部错误处理机制,捕获了该错误,然后进行组件卸载,防止显示错误UI。下图是React的渲染流程:
主要可以分为两个阶段:
Render阶段
根据 JSX转换的ReactElement节点 +旧的Fiber节点树 构建出新的Fiber节点树(Fiber可以被看作为是虚拟DOM)。
Commit阶段
根据Render阶段得出的新的Fiber树更新DOM。
渲染触发时机
- 组件初始化。
- 通过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 阶段调用的是handleError ,Commit 阶段调用的是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>;
}
上述代码的执行流程是:
- 编译时 :JSX 转变为React.createElement/_jsx,创建ReactElement的函数语句.
- 运行时 :组件 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内部的错误处理机制捕获,从而触发的一种主动防御的机制。