React 的状态更新是驱动组件重渲染的核心手段。理解 useState 和 setState 在底层如何工作、何时会导致重渲染,直接决定了你能否写出行为符合预期的组件。
函数组件的状态更新:useState
useState 返回一个状态值和一个更新函数。调用更新函数时,React 会将该组件的更新加入更新队列,并在合适的时机重新执行组件函数,从而得到新的 UI。
更新值的两种方式:直接传值 vs 函数式更新
const [count, setCount] = useState(0);
// 方式一:直接传入新值
setCount(5); // count 直接变为 5
// 方式二:传入基于前值的函数
setCount(prev => prev + 1); // 新值依赖旧值计算得到
这两种方式的差异在连续多次调用时会被放大。由于 React 会对状态更新进行批处理(见 6.2 节),直接传值的连续调用并不会叠加:
// 假设当前 count = 0
setCount(count + 1); // 期望 count = 1
setCount(count + 1); // 期望 count = 2,实际此时 count 仍为 0,最终结果为 1
而函数式更新始终基于最新的前值,因此可以安全地连续调用:
setCount(prev => prev + 1); // 基于旧值 0,产生 1
setCount(prev => prev + 1); // 基于旧值 1,产生 2
实用建议:只要新状态依赖旧状态计算,就使用函数式更新,避免因闭包中的“过期”状态导致意外。
类组件的状态更新:setState
类组件使用 this.setState 进行状态更新。它的更新机制与 useState 有相似之处,但存在关键差异:
- 对象式合并:
setState会浅合并传入的对象到当前 state 中,而不会完全替换整个 state 对象。 - 函数式更新:同样支持传入函数
(prevState, props) => newState。
class Counter extends React.Component {
state = { count: 0, label: '点击次数' };
increment = () => {
// 对象式:只会更新 count,label 会保留
this.setState({ count: this.state.count + 1 });
// 函数式:基于最新 state 安全更新
this.setState(prev => ({ count: prev.count + 1 }));
};
}
类组件与函数组件的核心区别:函数组件的
useState是替换式更新,每次更新都会用新值覆盖旧值(即使旧值是一个对象);类组件的setState是合并式更新,自动合并顶层字段。因此,在函数组件中存储对象状态时,需要自行扩展旧状态:
const [user, setUser] = useState({ name: 'Alice', age: 25 });
setUser(prev => ({ ...prev, age: 26 })); // 如果不展开,name 会丢失
状态更新是同步还是异步的?
这是 React 开发者最容易混淆的问题之一。实际行为取决于调用场景和 React 版本:
在 React 17 及之前:
- 合成事件和生命周期函数内:
setState/useState更新是异步批处理的。多次调用会被合并为一次更新,无法在调用后立即读取到更新的状态值。 - 原生事件、setTimeout、fetch 回调等异步代码内:更新是同步的,每次调用都会立即触发重渲染(跳过批处理),因此可以立即读取到最新 state。
// React 17 中
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出旧值(异步更新)
};
handleAsync = () => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出新值(同步更新)
}, 0);
};
在 React 18 中:
- 所有场景都默认开启自动批处理(Automatic Batching)。即使在
setTimeout、Promise、原生事件等异步回调中,多次状态更新也会被合并成一次重渲染。这一改变极大地减少了不必要的渲染次数,同时让行为更具一致性。 - 需要立即获取更新后的值,仍须通过函数式更新或
useEffect/componentDidUpdate等方式。
// React 18 中
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1); // 两次更新被合并为一次,最终只触发一次重渲染
}, 0);
如何正确获取更新后的状态
由于更新可能是异步的,不能依赖在更新代码后直接读取状态。正确的做法:
- 用于计算新状态:始终使用函数式更新(
prev => ...)。 - 用于执行副作用:将依赖该状态的逻辑放入
useEffect,依赖数组中声明该状态。每次状态变化后,副作用都会执行。 - 需要跨渲染保存最新值:使用
useRef存储可变值,而不触发重渲染。
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count; // 每次渲染后更新 ref 内的最新值
const handleClick = () => {
setCount(prev => prev + 1);
// 需要获取最新值调用 API 时,使用 ref.current
fetchData(countRef.current);
};
状态更新触发重渲染的边界
React 通过 Object.is 比较新旧状态来决定是否跳过重渲染。如果更新后的值与当前值相同(Object.is 返回 true),React 会跳过该组件的渲染及其子树的渲染。
const [count, setCount] = useState(0);
setCount(0); // 与当前值相同,不会触发重渲染
注意:对于对象或数组,即使内部内容相同,只要引用地址不同,React 就会触发重渲染。因此需要配合 useMemo 或 useCallback 来避免不必要的引用变化。
理解这些机制之后,你就能在开发中有信心地控制 “何时渲染” 以及 “如何拿到正确数据”,这是写出高性能、行为可预测的 React 应用的基础。