React的setState的使用

在React类组件开发中,使用setState 用于更新组件的状态。它是一个异步操作,它会将新的状态合并到当前状态中,然后触发组件的重新渲染。那么为什么要使用它来更新状态呢?

为什么使用setState

大家都知道,在开发中我们并不能直接通过修改state的值来让界面重新渲染,因为this.state这种方式修改的状态,React并不知道数据发生了变化,需要通过setState来通知React状态已经发生了改变。那在组件中,我们并没有去实现setState方法,为啥可以在组件中调用呢?那是因为setState方法是从Component中继承过来的。

setState方法
以上可以看到, 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连续更新操作

连续的多个 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> 
  }
}

浏览器效果
setState多次连续执行
上面当点击按钮改变状态时,执行handleClick方法里的for循环,可以看出连续执行了10次setState方法,但最后的结果确是1。这是为什么呢?

这是因为在每一轮循环的时候,count的状态并有更新,只是把修改的任务放到了update队列中,所以每一次循环,获取的this.state.count都是初始值0,因此,放入队列中的任务都是把count修改为1,最后把所有的setState合并成一个执行,最后结果输出就是1。
如下图:
setState添加到队列

为了解决这个问题,可以使用回调函数作为 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多次连续执行
在上面方法中,使用回调函数的方式进行连续的状态更新,这样每次更新都基于前一个更新的结果,避免了合并问题,这种方式保证了状态的正确更新。

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> 
  }
}

浏览器效果
setState同步
点击按钮触发事件,发现 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同步
点击按钮,会发现每次 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 的调用来同步更新状态,确保在执行下一个任务之前立即获取到更新后的状态。


 上一篇
React的合成事件 React的合成事件
什么是React的合成事件 React 合成事件(SyntheticEvent)是 React 框架提供的一种事件对象,它封装了不同浏览器的事件对象,使得开发者可以编写跨浏览器兼容的代码。合成事件对象中,也包含了浏览器内置事件对象中的一些属
2022-04-10
下一篇 
React的类组件ref使用 React的类组件ref使用
React提供了ref属性可以让我们可以引用组件的实例,其实就是ReactDOM.render()返回的组件实例,可以通过ref来获取DOM元素。 初次使用 在React中,ref可以挂载到html元素上,同时也可以挂载在React元素上,
2022-04-09
  目录