useState 与状态快照
useState 与状态快照 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课解决一个几乎每个 React 学习者都会踩的坑:连续调用两次 setCount(count + 1),界面却只加了一次。读完你应该能解释为什么会这样,并且知道什么时候必须换成函数式更新。
问题场景
想象你在写一个学习计时器,按钮「加 5 分钟」的实现是一次性加 5。第一版代码是循环调用五次 setMinutes(minutes + 1):
function addFiveBroken() {
for (let i = 0; i < 5; i++) {
setMinutes(minutes + 1); // 结果:只加了 1
}
}
点完按钮,分钟数从 0 变成了 1,不是 5。逻辑看起来没问题,结果却不对——问题不在循环,而在于 minutes 这个变量在这次函数执行期间是「旧」的。
核心解释
要理解这个现象,先接受一个模型:状态是快照,不是实时变量。
一次渲染(render)就是 React 调用你的组件函数一次。调用时传入的 minutes 是那次渲染时的状态值,好比一张照片。在这次函数执行的全程里,这张照片不会更新——哪怕你中途调用了 setMinutes,也只是「预约」了下一次渲染,当前照片不变。
console.log(minutes); // 0(第 0 次渲染的快照)
setMinutes(1); // 通知 React:下一次渲染用 1
console.log(minutes); // 还是 0,快照不会中途改变
所以循环里的五次 setMinutes(minutes + 1) 实际都是 setMinutes(0 + 1)——五次都预约「下一次变成 1」,最终结果自然就是 1。React 还会把同一个事件处理函数里的多次 setState 合并成一次渲染(批处理),所以连界面刷新都只有一次。
修复方法是函数式更新:给 setState 传一个函数,参数是「最新的状态值」,返回「下一个状态值」。
function addFiveFixed() {
for (let i = 0; i < 5; i++) {
setMinutes(m => m + 1); // 每次 m 都是上一次计算的结果
}
}
这五次更新会排进一个队列:0 => +1 => +1 => +1 => +1 => +1,最终正确得到 5。React 官方把这个队列机制叫「更新队列」(update queue),你不需要手工管理它,只需要记住:新状态依赖旧状态时,一律用函数式更新。
示例拆解
一个最小可运行的对照实验,两个按钮分别用两种写法:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前:{count}</p>
{/* 错误示范:连点三次也只加 1 */}
<button onClick={() => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }}>
快照式 +1(点三次试试)
</button>
{/* 正确做法:函数式更新,点三次真的加 3 */}
<button onClick={() => { setCount(c => c + 1); setCount(c => c + 1); setCount(c => c + 1); }}>
函数式 +3
</button>
</div>
);
}
逐行看关键点:
useState(0)只在首次渲染真正「初始化」,之后调用它返回的都是当次渲染的快照。- 第一个按钮的三次
setCount(count + 1)用的是同一个快照count,三次都预约count + 1,结果只加 1。 - 第二个按钮的
c => c + 1里,c是队列里前一步算出的值,三次排队执行,结果加 3。
动手任务
打开本课实验区,完成两个小改动:
- 把「快照式 +1」按钮改成点击后真的加 2(不许改成函数式,试试还有没有别的办法——提示:直接写
setCount(count + 2))。 - 新增一个「重置」按钮,用函数式更新把计数归零。
改完在实验区点按钮验证:快照式按钮点三次 +2 应该只加 2,函数式 +3 每次都加 3。
常见错误
- 在 setState 之后立刻读取状态:
setCount(1); console.log(count)打印的还是旧值。状态更新发生在下一次渲染,不是当场生效。 - 在函数式更新里写副作用:
setCount(c => { fetch(...); return c + 1 })—— 更新函数必须是纯计算,副作用应该放在事件处理或 useEffect 里。 - 把函数式更新当成性能优化:它不是为了省一次渲染,而是为了正确性。两种写法渲染次数一样。
- 依赖旧快照做条件判断:
if (count < 10) setCount(count + 1)在快速连点时判断依据是旧快照,应该写成setCount(c => (c < 10 ? c + 1 : c))。
检查与复盘
完成下方检查题确认理解。自查清单:
- 能用自己的话说清「快照」是什么,为什么
setCount(count + 1)连点会丢更新 - 知道批处理:同一个事件里的多次 setState 合并成一次渲染
- 写代码时能不假思索地选择:新值依赖旧值 → 函数式更新
参考资料
- React 官方文档:State 如同一张快照 —— 快照模型的权威解释,建议读完本课后对照阅读
- React 官方文档:队列化一系列状态更新 —— 更新队列与函数式更新的细节
读完这一课了?
标记完成后,工作台会安排接下来的学习。