语法:
const [state, setState] = useState(initialState)
参数:
-
setState 它有两种写法
setState(value) setState(pre => pre + 1)
下面来详细了解一下它为什么会有两种写法。
setState(value)
先看一个例子:
const App = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
return <div>
<h1>{count}</h1>
<div onClick={handleClick}>点击</div>
</div>
}
当点击按钮时,最终页面上展示的会是:1
为什么呢?
-
state的闭包陷阱(Stale Closure)
handleClick 函数作用域内的count值,永远是当前渲染时的值,初始值 0
-
异步和批处理机制(Async & Batching):
React 会将
同一个事件处理器中的所有setCount调用批量处理,而不是立即更新 -
计算过程:
- 当前渲染周期中
count = 0 - 第一次
setCount(count + 1):计划将count更新为1 + 0 = 1 - 第二次
setCount(count + 1):此时的count仍然是 1(不是更新后的 2),所以还是计划更新为1 + 0 = 1 - 第三次同理,仍然是计划更新为 1
- React 合并所有更新,最后只执行一次更新:
setCount(count + 1)
- 当前渲染周期中
因此引入了 函数式写法 setState(pre => pre + 1)
setState(pre => pre + 1)
还是上面的例子:
const App = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
}
return <div>
<h1>{count}</h1>
<div onClick={handleClick}>点击</div>
</div>
}
此时,当点击按钮时,最终页面上展示的会是:3
为什么呢?我们来看一下执行过程
- 点击
- 出发函数 handleClick
- 3个更新函数依次进入队列:setCount(prev => prev + 1) 入队
- fiber架构整个渲染过程,render阶段执行计算
- 开始处理 更新队列 (初始值 state = 0)
- 执行函数一:0 + 1 = 1
- 执行函数二:此时pre = 1,故 1 + 1 = 2
- 执行函数三:此时pre = 2,故 2 + 1 = 3
- 最终 count = 3
- Commit 阶段:
count = 3被用于渲染,更新到DOM
函数式更新的优势:
- 不受闭包影响,它可以保证数据更新的准确性
- 代码更直观
如何选择:
新state依赖于旧state,那么我们应该用函数式更新- 如果没有依赖关系,我们则使用 直接传值的方式;
批处理
批处理指的是 React 会将多个状态更新合并到一次重新渲染中,而不是每次 setState 都立即触发重新渲染。
原则
-
事件处理器中的自动批处理
在 React 事件处理器(如 onClick、onChange)中,所有的
setState调用都会被自动批处理。理解:
同一个事件处理器中的 setState 会被批处理,而不是单个单个执行同一个事件处理器 -
生命周期和 useEffect 中的批处理
在生命周期方法和
useEffect中,更新也会被批处理。 -
React 18 的增强批处理
React 18 之前:只有在 React 事件处理器中才有自动批处理。
React 18 及以后:批处理扩展到 所有场景,包括:
- Promise
- setTimeout
- 原生事件处理程序
- 其他异步代码