2、useState 深度解析:状态更新的奥秘,惰性初始化的妙用,与 Class 组件的 setState 有何不同?

好,咱们今天来聊聊 useState。这个 Hook 可以说是 React 函数组件的基石,几乎每个组件都会用到它。但说实话,很多人只是「会用」,并没有真正理解它背后的机制。

我个人习惯把 useState 比作一个「状态小盒子」。你往里面放东西,它帮你存着,你取出来用,它还能通知 React 去更新视图。但这个小盒子,比 Class 组件的 setState 要聪明得多。

状态更新的奥秘:异步还是同步?

先问大家一个问题:useState 的更新是同步还是异步?

嗯,这个问题其实是个陷阱。它既不是纯粹的同步,也不是纯粹的异步。准确地说,它是「批量更新」的

看个例子:

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  return <button onClick={handleClick}>{count}</button>;
}

点击一次按钮,count 会变成多少?

答案是 1,不是 3。

为什么会这样?因为 React 会把三次 setCount 合并成一次更新。每次 setCount 拿到的 count 都是 0,所以三次都是 0 + 1,最终结果就是 1。

核心要点:在同一个事件处理函数或生命周期中,多次调用 setState 会被合并成一次更新。

那如果我想基于上一次的状态来更新呢?用函数式更新:

function handleClick() {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
}

这次点击后,count 就是 3 了。因为每次更新都基于前一次的结果。

我在项目中遇到过一个问题:有个同事在 setTimeout 里调用 setCount,结果发现状态没有立即更新。这是因为 setTimeout 不在 React 的批量更新上下文中。React 18 之后,这种情况也被优化了,会自动批量更新。

惰性初始化的妙用

这个特性,说实话,很多人没用过,甚至不知道。但用好了,能省不少性能。

看这个例子:

// 不好的写法
const [state, setState] = useState(expensiveComputation());

// 好的写法
const [state, setState] = useState(() => expensiveComputation());

第一种写法,每次组件渲染都会执行 expensiveComputation(),哪怕这个值只在初始化时用一次。第二种写法,传入一个函数,React 只在首次渲染时调用这个函数,后续渲染直接忽略。

小技巧:如果你的初始状态需要经过复杂计算才能得到,比如解析 localStorage 里的数据、处理大量数组等,一定要用惰性初始化。

我曾经接手过一个项目,里面有个表格组件,每次渲染都要解析一个巨大的 JSON 数据作为初始状态。页面卡得不行。我改成惰性初始化后,性能直接起飞。嗯,有时候优化就是这么简单。

与 Class 组件的 setState 有何不同?

这个问题,面试经常问。我总结了一下,主要有三点不同:

对比维度 useState Class 组件的 setState
合并方式 替换整个状态 浅合并(shallow merge)
更新粒度 每个状态独立 整个 state 对象
函数式更新 支持,且是推荐做法 支持,但用得少
惰性初始化 原生支持 需要手动实现

说白了,useState 更「原子化」。每个状态都是独立的,你更新 count 不会影响 name。而 Class 组件的 setState 是浅合并,你更新 this.state.count 时,其他字段还在。

举个例子:

// Class 组件
this.state = { count: 0, name: 'React' };
this.setState({ count: 1 });
// 结果:{ count: 1, name: 'React' }  —— name 还在

// 函数组件
const [count, setCount] = useState(0);
const [name, setName] = useState('React');
setCount(1);
// 结果:count 变成 1,name 不受影响

我个人更喜欢 useState 的方式。你想想看,每个状态独立管理,逻辑更清晰,也更容易拆分和复用。

避坑指南:我曾经在 Class 组件里犯过一个错——用 this.setState({ count: this.state.count + 1 }) 连续调用三次,结果只加了一次。因为 this.state.count 在每次调用时都是同一个值。后来改用函数式更新才解决。这个坑在 useState 里同样存在,所以记住:如果新状态依赖旧状态,一定要用函数式更新

总结一下

嗯,今天的内容就这些。咱们回顾一下:

  • 状态更新是批量处理的,多次调用会被合并
  • 惰性初始化能避免不必要的计算,提升性能
  • 与 Class 组件的 setState 相比,useState 更原子化,支持惰性初始化,更新方式是替换而非合并

下一章,咱们聊聊 useEffect 的完整指南。这个 Hook 比 useState 更容易踩坑,到时候我会分享一些实战中的血泪史。