前言
在 React 中,useState 可以接收两种类型的参数:
-
普通初始值
scssconst [state, setState] = useState(0); -
懒初始化函数
scssconst [state, setState] = useState(() => computeExpensiveValue());
懒初始化函数的核心目标是:避免在非首次渲染时执行昂贵的初始计算。
1. 懒初始化函数的行为
用一个例子对比两种写法:
scss
function expensiveInit() {
console.log('计算初始值');
return 42;
}
// 普通方式
const [count1, setCount1] = useState(expensiveInit());
// 懒初始化方式
const [count2, setCount2] = useState(() => expensiveInit());
输出顺序:
- 普通方式:
计算初始值每次渲染都会打印 - 懒初始化方式:
计算初始值只在首次渲染打印一次
为什么会这样?我们需要看 React 的底层工作机制。
2. React 内部是如何处理的
React 使用 Fiber 架构 来管理函数组件的状态。每个组件对应一个 Fiber 节点,useState 通过 hook 链表存储状态信息。
2.1 useState 底层步骤
-
创建或获取 hook 对象
每次调用
useState,React 会检查当前 Fiber 的 hook 链表是否已经存在对应的 hook:- 首次渲染:创建 hook 对象,存储状态和更新队列
- 更新渲染:直接从 hook 链表读取上一次的状态
-
判断传入参数类型
-
如果传入 函数,React 会 判断是否是第一次执行:
iniif (typeof initialState === 'function') { hook.memoizedState = initialState(); // 仅首次渲染执行 } else { hook.memoizedState = initialState; // 直接使用值 } -
如果是普通值,直接赋值给
hook.memoizedState。
-
-
更新机制
当
setState被调用时,React 会把新的状态加入更新队列,然后调度重新渲染:- 再次渲染时 不会再次执行懒初始化函数
- React 会直接使用 hook 中存储的状态或更新队列计算的新状态
2.2 Fiber 链表与 hook 链表
每个函数组件的状态存储在一个 hook 链表 中:
rust
FiberNode
├── memoizedState -> Hook (useState)
├── memoizedState -> Hook (useState)
└── ...
每个 Hook 对象结构大概如下:
yaml
{
memoizedState: 初始状态或最新状态,
queue: 状态更新队列,
next: 下一个 Hook
}
-
首次渲染:
- React 会遍历 hook 链表,创建每个 hook 对象
- 如果 hook 接收的是函数,React 会立即执行它来获得初始状态
-
更新渲染:
- React 直接读取 hook 链表里的状态
- 传给
useState的函数 不会再执行
3. 为什么使用懒初始化
假设我们有一个复杂计算:
bash
function expensiveInit() {
let sum = 0;
for (let i = 0; i < 1e8; i++) sum += i;
return sum;
}
-
如果写成普通方式:
scssconst [value, setValue] = useState(expensiveInit());每次渲染都会执行这个循环,导致性能严重下降。
-
如果使用懒初始化:
scssconst [value, setValue] = useState(() => expensiveInit());循环只在首次渲染执行一次,后续渲染直接复用状态。
4. 总结
所谓的懒初始化 是通过向
useState传入函数,使得初始化逻辑只在首次渲染时由 React 调用,避免组件每次执行时都提前计算初始值,从而减少不必要的性能开销。(懒初始化优化的不是 React 的行为,而是 JS 求值时机。)