React Fast Refresh 实现原理
React Fast Refresh 是 React 官方推荐的 HMR 解决方案,旨在提供快速、可靠的热更新体验,同时保留组件的本地状态(例如 useState
和 useRef
的值)。它由 Facebook 团队开发,广泛集成到现代构建工具(如 Vite 和 Next.js)中。
背景与目标
传统 HMR 的问题
- 在传统的 HMR 实现中,当模块更新时,整个组件树可能会被重新渲染,导致本地状态丢失。
- 对于 React 组件,状态丢失会影响开发体验,例如表单输入值或动画状态被重置。
React Fast Refresh 的目标
- 提供快速的模块更新。
- 保留 React 组件的本地状态。
- 只重新渲染受影响的组件,而不是整个应用。
核心原理
React Fast Refresh 通过在编译时注入特定的运行时代码,并在运行时与 React 协调器(React Reconciler)协作,实现高效的热更新。
编译时注入
工具支持:
- React Fast Refresh 依赖于 Babel 插件(
react-refresh/babel
)在编译时处理源代码。 - Vite 通过
@vitejs/plugin-react
集成这个功能。
- React Fast Refresh 依赖于 Babel 插件(
注入的内容:
为每个函数组件(Function Component)注入唯一的标识符(ID)和 HMR 逻辑。
示例(简化后的代码):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 原始代码
function MyComponent() {
const [count, setCount] = useState(0)
return <div>{count}</div>
}
// 编译后注入的代码
import { $RefreshReg$, $RefreshSig$ } from "react-refresh/runtime"
function MyComponent() {
const [count, setCount] = useState(0)
return <div>{count}</div>
}
$RefreshReg$(MyComponent, "MyComponent") // 注册组件
$RefreshSig$() // 签名,用于检测变化
作用:
$RefreshReg$
:将组件注册到 Fast Refresh 运行时,关联一个唯一的 ID。$RefreshSig$
:生成组件的签名,用于检测代码变化。
运行时管理
- React Refresh Runtime:
- Fast Refresh 提供了一个运行时库(
react-refresh/runtime
),由构建工具注入到客户端。 - 这个运行时负责与 React 协调器协作,管理组件的更新。
- Fast Refresh 提供了一个运行时库(
- 组件注册:
- 在应用启动时,所有组件通过
$RefreshReg$
注册到运行时,记录其初始定义和签名。
- 在应用启动时,所有组件通过
- 签名检测:
- 每次模块更新时,运行时比较新旧代码的签名。
- 如果签名未变(例如只修改了无关逻辑),组件不会重新注册。
- 如果签名变化(例如修改了
useState
调用),运行时标记组件需要更新。
HMR 集成
- Vite 的 HMR 机制:
- Vite 检测到文件变化后,通过 WebSocket 通知客户端。
- 对于 React 文件(
.jsx
或.tsx
),Vite 编译新代码并发送 HMR 更新事件。
- Fast Refresh 的处理:
- 客户端的 HMR 运行时(/@vite/client)接收到更新后,调用 react-refresh/runtime 的 API。
- 运行时执行以下步骤:
- 重新加载模块:
- 使用动态
import()
加载更新后的模块。
- 使用动态
- 比较签名:
- 检查新模块中的组件签名是否与旧签名匹配。
- 更新组件:
- 如果签名匹配,运行时通知 React 重新渲染受影响的组件。
- 如果签名不匹配(例如添加了新的 Hook),运行时触发全组件刷新,但保留状态。
- 重新加载模块:
状态保留
- React Fiber 的支持:
- React Fast Refresh 利用 React 的 Fiber 架构(React 的内部协调机制)。
- Fiber 节点保存了组件的实例和状态(
memoizedState
)。 - 当组件更新时,Fast Refresh 告诉 React 重用现有的 Fiber 节点,而不是重新创建。
- 条件:
- 只有函数组件支持状态保留(类组件不支持)。
- Hooks 的调用顺序必须保持一致(React 的 Hook 规则)。
限制与边界
支持的组件
- 只支持函数组件和 Hooks,不支持类组件。
边界情况
- 如果修改了 Hook 的调用顺序或数量,Fast Refresh 无法保留状态,会触发全组件刷新。
- 如果模块没有显式接受 HMR(例如没有注入 Fast Refresh 逻辑),Vite 会触发全页面刷新。
错误处理
- Fast Refresh 会在控制台输出警告,提示开发者修复代码以避免状态丢失。
性能优化
- 增量更新: 只重新渲染受影响的组件,而不是整个应用。
- 快速编译: Vite 使用 Esbuild 快速编译 JSX/TSX 文件,减少更新延迟。
- 签名优化: 签名比较避免不必要的组件更新,提高性能。