useState详解,摸鱼仔无事,重新再看看常用react hooks
1. useState 基础用法
const [count, setCount] = useState(0);
setCount(count + 1);
关键点补充解释:
-
useState在组件首次渲染时初始化 -
后续渲染不会再次执行初始化函数
-
setCount并不是“直接改值” ,而是:向 React 提交一次“更新请求”
这一点非常重要,后面所有“异步 / 批处理 / 丢更新”问题都源自这里。
2️. useState 为什么“异步”?
❗一个常见误区
❌ setState 是 JS 异步
✅ setState 是 React 延迟执行
真正的原因只有一句话:
React 会先收集更新,再统一渲染,而不是来一个更新就渲染一次
一个更贴切的类比
把 setState 想成:
React.enqueueUpdate(update)
而不是:
state = newState
你调用的是 “登记更新” ,不是“立刻修改”。
3️. 批处理(Batching)到底在干什么?
React 18 之前 vs 之后
React 17 及以前
仅在 React 事件中 批处理:
onClick={() => {
setA(1)
setB(2)
}} // 批处理
setTimeout(() => {
setA(1)
setB(2)
}) // 不批处理(渲染两次)
React 18(Automatic Batching)
任何地方都会批处理
setTimeout(() => {
setA(1)
setB(2)
}) // 只渲染一次
Promise.resolve().then(() => {
setA(1)
setB(2)
}) // 渲染一次
React 18 引入了 Automatic Batching
🔬 Demo:观察渲染次数
import { Button } from "antd";
import { useState } from "react";
export default function FuncUpdateDemo() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
console.log("render");
const handleClick = () => {
setA((prevA) => prevA + 1);
setA((prevA) => prevA + 1);
setB((prevB) => prevB + 1);
};
return (
<div className="m-[10px]">
<div>
<p>a: {a}</p>
<p>b: {b}</p>
</div>
<div>
<Button type="primary" onClick={handleClick}>
a + 1
</Button>
</div>
</div>
);
}
点击一次:
render
只打印一次,a: 2 ,b:1
4️. setState 到底什么时候“同步”?什么时候“异步”?
核心判断标准
React 是否能控制这段代码的执行上下文
| 场景 | 是否批处理 | 原因 |
|---|---|---|
| React 事件 | ✅ | React 包了一层 |
| Promise / async | ✅(React 18) | 自动批处理 |
| setTimeout | ✅(React 18) | 自动批处理 |
| 原生 DOM 事件 | ❌ | React 管不到 |
| flushSync | ❌ | 人为打断批处理 |
Demo:原生事件同步更新
useEffect(() => {
const btn = document.getElementById('btn');
btn!.addEventListener('click', () => {
setCount(c => c + 1);
console.log(count); // 最新值
});
}, []);
因为 不在 React 管理范围内
用ref拿最新值,不触发render
const aRef = useRef(0);
const handleClick = () => {
aRef.current++;
console.log(aRef.current);
};
5️. useState 更新流程
一次 setState 发生了什么?
setState
↓
创建 Update 对象
↓
放入 Fiber.updateQueue
↓
标记 Fiber 为 dirty
↓
Scheduler 调度(优先级)
↓
Render Phase(计算 JSX)
↓
Commit Phase(更新 DOM)
为什么 React 要“等一等”再渲染?
因为 React 支持:
- 优先级(用户输入 > 网络返回)
- 中断渲染
- 并发模式(Concurrent Rendering)
- 避免频繁渲染,性能优化
6️. 常见陷阱
① 连续 setState 丢更新
const [count, setCount] = useState(0);
setCount(count + 1);
setCount(count + 1);
//count 最终只+1 count=1而非2
为什么?
- 两次都读的是 同一次 render 的 count
- React 只是合并了 update,不是重新取值
正确写法
const [count, setCount] = useState(0);
setCount(c => c + 1);
setCount(c => c + 1); //count=2
② 闭包陷阱
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
}, []);
❌ 永远是初始值
正确方式 1:函数式更新
setCount(c => {
console.log(c);
return c + 1;
});
正确方式 2:useRef 保存最新值
const countRef = useRef(count);
countRef.current = count;
正确方式 3:依赖值变化
useEffect(() => {
setInterval(() => {
console.log(count);
}, 1000);
}, [count]);
③ useState 不会合并对象(和 class 不同)
const [state, setState] = useState({ a: 1, b: 2 });
setState({ a: 2 }); // b 丢了
正确:
setState(prev => ({ ...prev, a: 2 }));
7️. 深入:为什么批处理是 React 的“命根子”?
假设没有批处理:
setA(1);
setB(2);
setC(3);
3 次 render + 3 次 diff + 3 次 commit
页面会抖,CPU 会炸。
批处理的意义一句话总结:
牺牲“立刻可见”,换取“整体性能”
8️. 最终总结
useState不是 JS 异步,而是 React 延迟调度(react scheduler)- 批处理是 React 性能的基石
- React 18 默认开启 Automatic Batching
- 连续更新一定用函数式 setState (setState((prev)=>prev+1))
- 闭包问题本质是“渲染快照”,(拿到的是旧state ‘旧照片’)
🔚彩蛋:一句面试必杀回答
setState 为什么是异步的?
因为 React 要收集多次更新,统一调度渲染,从而减少重复 render,并支持并发和优先级控制。