在React类组件开发中,使用setState
用于更新组件的状态。它是一个异步操作,它会将新的状态合并到当前状态中,然后触发组件的重新渲染。那么为什么要使用它来更新状态呢?
为什么使用setState
大家都知道,在开发中我们并不能直接通过修改state
的值来让界面重新渲染,因为this.state
这种方式修改的状态,React并不知道数据发生了变化,需要通过setState
来通知React状态已经发生了改变。那在组件中,我们并没有去实现setState
方法,为啥可以在组件中调用呢?那是因为setState
方法是从Component
中继承过来的。
以上可以看到, setState
是放在Component
的原型上的。
基本用法
setState
的基本用法是第一个参数是一个对象的形式:this.setState({key:value});
下面代码展示:
class Parent extends React.Component{
state = {
count:0
}
handleClick = () =>{
this.setState({
count: this.state.count + 1
})
}
render(){
return <div>
<h2>React的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button onClick={this.handleClick}>改变状态</button>
</div>
</div>
}
}
上面是以对象的形式修改状态。
使用函数作为参数
setState
的第一个参数还可以是一个函数,该函数参数接收两个参数:prevState
和 props
。
- prevState:表示组件当前的状态。
- props:表示组件的属性。
在函数内部,可以使用这两个参数来计算新的状态对象,然后将其返回。React 会使用这个新状态对象合并到当前状态中,并在合适的时机触发组件的重新渲染。
this.setState((prevState, props) => {
// 在这里可以基于 prevState 和 props 计算新的状态
return {newState};
});
下面代码以函数形式修改状态:
class Parent extends React.Component{
state = {
count:0
}
handleClick = () =>{
this.setState((preState,props)=>{
//可以直接在回调函数中获取改变前的preState和props
console.log("preState:", preState);
console.log("props信息", props);
//对数据进行其他的计算
const count = preState.count + 1
//该回调函数返回一个对象
return {
count
};
})
}
render(){
return <div>
<h2>React的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button onClick={this.handleClick}>改变状态</button>
</div>
</div>
}
}
以上setState
第一个参数传入的是一个函数,在函数内对数据状态进行改变或其他操作,最后返回一个对象。
setState传入第二个参数callback
因为setState
是一个异步的过程,所以说执行完setState
之后不能立刻更改state
里面的值。如果需要对state
数据更改监听,setState
的第二个参数,就是用来监听state
里面数据的更改,当数据更改完成,调用回调函数,用于可以实时的获取到更新之后的数据。
class Parent extends React.Component{
state = {
count:0
}
handleClick = () =>{
this.setState({
count: this.state.count + 1
},()=>{
console.log('在setState回调函数中输出count:',this.state.count)
})
console.log('执行setState后立即输出count:',this.state.count)
}
render(){
return <div>
<h2>React的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button onClick={this.handleClick}>改变状态</button>
</div>
</div>
}
}
浏览器效果
setState连续更新操作
连续的多个 setState
调用会在同一批次中被合并执行,导致只有最后一个调用的状态更新生效。
class Parent extends React.Component{
state = {
count:0
}
handleClick = () =>{
for(let i = 0;i < 10; i++){
this.setState({
count: this.state.count + 1
},()=>{
console.log('执行了第'+i+'次')
})
}
}
render(){
return <div>
<h2>React的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button onClick={this.handleClick}>改变状态</button>
</div>
</div>
}
}
浏览器效果
上面当点击按钮改变状态时,执行handleClick
方法里的for
循环,可以看出连续执行了10次setState
方法,但最后的结果确是1
。这是为什么呢?
这是因为在每一轮循环的时候,count
的状态并有更新,只是把修改的任务放到了update队列中,所以每一次循环,获取的this.state.count
都是初始值0,因此,放入队列中的任务都是把count
修改为1,最后把所有的setState
合并成一个执行,最后结果输出就是1。
如下图:
为了解决这个问题,可以使用回调函数作为 setState
的参数,将状态更新逻辑嵌套在回调函数内。
class Parent extends React.Component{
state = {
count:0
}
handleClick = () =>{
for(let i = 0;i < 10; i++){
this.setState((preState) => {
const count = preState.count + 1
return {
count
}
},()=>{
console.log('执行了第'+i+'次')
})
}
}
render(){
return <div>
<h2>React的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button onClick={this.handleClick}>改变状态</button>
</div>
</div>
}
}
浏览器效果
在上面方法中,使用回调函数的方式进行连续的状态更新,这样每次更新都基于前一个更新的结果,避免了合并问题,这种方式保证了状态的正确更新。
setState为什么是异步
React 的 setState
是异步的设计是出于性能优化和避免不必要的渲染的考虑。
- 性能优化: 当调用
setState
来更新组件的状态时,React 并不会立即进行重新渲染。而是会将更新放入update队列中,然后在合适的时机批量处理这些更新,从而减少不必要的重复渲染。这样可以提高性能,避免频繁的 DOM 操作。 - 合并更新:React 会合并相邻的
setState
调用,避免重复的渲染。这意味着如果在一次事件处理函数中多次调用了setState
,React 会将这些调用合并成一个更新操作,只触发一次重新渲染。 - 批量更新:当多个组件都有状态更新时,React 会将这些更新批量处理,然后一次性更新所有组件,避免了不必要的重复渲染和频繁的 DOM 操作。
setState一定是异步吗
setState
是否是异步需要根据React的版本来确定,在React18中,setState
操作一定是异步的。在React18版本之前,在组件生命周期或React合成事件中,setState
是异步。在一些特殊情况下,setState
可能会被视为同步操作。
使用 setTimeout
的时候
//"react": "^16.14.0"执行下面代码
class Parent extends React.Component {
state = {
count: 0,
}
handleClick = () =>{
this.setState({count: this.state.count + 1})
console.log(this.state.count)//0
this.setState({count: this.state.count + 1})
console.log(this.state.count)//0
setTimeout(() => {
console.log(this.state.count)//1
this.setState({count: this.state.count + 1})
console.log(this.state.count)//2
},0)
}
render(){
return <div>
<h2>React16.14.0的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button onClick={this.handleClick}>改变状态</button>
</div>
</div>
}
}
浏览器效果
点击按钮触发事件,发现 setTimeout
里面的 count
值打印值为2,页面显示 count
的值为 2。setTimeout
里面 setState
之后能马上得到最新值。在 setTimeout
里面,setState
是同步的,经过前面两次的 setState
批量更新,count
值已经更新为 1。在 setTimeout
里面拿到最新的 count
值 1,执行setState
,然后能实时拿到 count
的值为2。
因此可以知道,在React18版本之前,使用 setTimeout
调用 setState
会导致同步更新,即在调用 setState
后立即执行重新渲染。这是因为在 setTimeout
中的代码会被推迟到下一个事件循环执行,而 React 的更新通常是在事件循环的末尾批量执行的,所以此时的 setState
可能会被立即处理。将 setState
的更新放在 setTimeout
的回调函数中。这样也能够在更新状态后立即访问到最新的状态。
在原生DOM事件
调用
//"react": "^16.14.0"执行下面代码
class Parent extends React.Component{
state = {
count:0
}
componentDidMount() {
document.querySelector('#btn').addEventListener('click', this.handleClick)
}
handleClick = () =>{
this.setState({count: this.state.count + 1})
console.log(this.state.count)//1
this.setState({count: this.state.count + 1})
console.log(this.state.count)//2
this.setState({count: this.state.count + 1})
console.log(this.state.count)//3
}
render(){
return <div>
<h2>React16.14.0的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button id='btn'>改变状态</button>
</div>
</div>
}
}
浏览器效果
点击按钮,会发现每次 setState
后打印出来的值都是实时拿到的,不会进行批量更新,所以在 DOM 原生事件里面,setState
是同步的。这是因为原生事件是在浏览器的事件循环中触发的,而 React 的更新也是在事件循环中执行的,因此在一些情况下,setState
调用可能会立即更新组件。
同步更新flushSync
尽管setState
默认以异步方式进行更新,但在某些情况下,您可能需要立即获取更新后的状态。为了实现此目的,React 18 提供了 flushSync
方法,可以强制执行同步更新。通过使用 flushSync
包裹 setState
的调用,您可以确保在执行下一个任务之前立即获取到更新后的状态。
下面是异步和同步状态更新的代码:
class Parent extends React.Component{
state = {
count:0
}
// 点击按钮后触发的方法,异步改变 count
handleClick = () =>{
this.setState({count: this.state.count + 1})
console.log(this.state.count)//0
}
// 点击按钮后触发的方法,同步改变 count
handleSyncClick = () =>{
flushSync(()=>{
this.setState({count: this.state.count + 1})
})
console.log(this.state.count)//1
}
render(){
return <div>
<h2>React的setState使用</h2>
<div>{this.state.count}</div>
<div>
<button onClick={this.handleClick}>异步改变状态</button>
<button onClick={this.handleSyncClick}>同步改变状态</button>
</div>
</div>
}
}
handleClick
方法被触发,点击 “异步改变状态” 按钮时,使用setState
异步更新count
的值为 1。然后打印console.log(this.state.count)
, 由于setState
是异步的,打印会显示之前的状态值0。handleSyncClick
方法被触发,点击 “同步改变状态” 按钮时,使用flushSync
函数同步更新count
的值为 1。然后尝试打印console.log(this.state.count)
,由于使用flushSync
后,状态同步更新,打印会显示最新的状态值1。
除了使用 flushSync
包裹 setState
的调用,还可以flushSync
单独使用,在setState
后执行flushSync
,也是可以立即执行update队列中的setState
。
//....
// 点击按钮后触发的方法,同步改变 count
handleSyncClick = () =>{
this.setState({count: this.state.count + 1})
console.log(this.state.count)//0
flushSync()// 执行flushSync 立即更新组件
console.log(this.state.count)//1
}
//....
在使用 flushSync
后,可以立即读取更新后的 DOM 状态,而不需要等待渲染完成。需要注意的是,flushSync
应该谨慎使用,因为它可能会阻塞渲染并影响性能。
总结
使用setState
时,如果新状态不依赖于原状态【使用对象方式】。
使用setState
时,如果新状态依赖于原状态 【使用函数方式】。
使用setState
时,setState
中的 preState
参数,总是能拿到即时更新的值。
使用setState
时,如果需要在setState()
执行后,获取最新的状态数据,可以使用第二个参数,在callback
函数中读取到异步更新的最新值。
使用setState
时,在React18版本前,在组件生命周期或React合成事件中,setState
是异步,在setTimeout
或者原生dom事件
中,setState是同步;在React18版本可以通过使用 flushSync
包裹 setState
的调用来同步更新状态,确保在执行下一个任务之前立即获取到更新后的状态。