React setState同步异步更新的场景

1,456 阅读3分钟

欢迎关注react源码系列一起学习源码呀。Github常更新,掘金不常更新。Github仓库中不仅有webpack生态源码,react源码,还有前端基础知识,八股文,踩坑经验等

setState同步更新还是异步更新?

  • React17稳定版本或者React17以前的版本中,即legacy模式下,在react能够接管的地方,比如生命周期或者合成事件中,setState是异步更新的。 但是在setTimeout或者通过window.addEventListener添加的原生事件中,setState则是同步的。
// Legacy同步模式
const container = document.getElementById('root');
ReactDOM.render(<App />, container);
  • 在React开发版本中,即concurrent模式下,setState的更新统一是异步的
// Concurrent异步模式,在这个模式下,任何情况下setState都是异步更新的。目前createRoot方法还在实验中
const container = document.getElementById('root');
// ReactDOM.render(<App />, container);
ReactDOM.createRoot(container).render(<App />)

setState更新场景

以下面的代码为例,以下示例均在react@17.0.1,react-dom@17.0.1版本的legacy模式下实现

import React from 'react';

class App extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      number: 0
    }
  }

  handleClick = event => {
    // todo
  }

  render(){
    console.log('render...', this.state)
    return (
      <div>
        计数器:{this.state.number}
        <div>
          <button onClick={this.handleClick}>add</button>
        </div>
      </div>
    )
  }
}
export default App;

setState异步更新场景

handleClick = event => {
    this.setState({ number: this.state.number + 1 }, () => {
      console.log('setState1 callback', this.state)
    })
    console.log('after setState1', this.state) // number: 0
    this.setState({ number: this.state.number + 1 }, () => {
      console.log('setState2 callback', this.state)
    })
    console.log('after setState2', this.state)  // number: 0
}

点击按钮,打印顺序:
after setState1 {number: 0}
after setState2 {number: 0}
render... {number: 1}
setState1 callback {number: 1}
setState2 callback {number: 1}

可以得出以下结论:

  • 状态是异步更新的
  • setState的回调函数是在状态更新后批量执行的

setState同步更新场景:setTimeout中

handleClick = event => {
    setTimeout(() => {
      this.setState({ number: this.state.number + 1 }, () => {
        console.log('setState1 callback', this.state)
      })
      console.log('after setState1', this.state) // number: 0
      this.setState({ number: this.state.number + 1 }, () => {
        console.log('setState2 callback', this.state)
      })
      console.log('after setState2', this.state)  // number: 0
    }, 4)
}

打印顺序:
render... {number: 1}
setState1 callback {number: 1}
after setState1 {number: 1}
render... {number: 2}
setState2 callback {number: 2}
after setState2 {number: 2}

可以得出以下结论

  • 状态是同步更新的,因此setState的回调也是同步执行的

注意观察render的打印时机以及次数!!!!

setState参数是函数的场景

handleClick = event => {
    this.setState((prevState) => {
      console.log('setState1...', prevState)
      return { number: prevState.number + 1 }
    }, () => {
      console.log('setState1 callback', this.state)
    })

    console.log('after setState1', this.state) 

    this.setState((prevState) => {
      console.log('setState2...', prevState)
      return { number: prevState.number + 1 }
    }, () => {
      console.log('setState2 callback', this.state)
    }) 
    console.log('after setState2', this.state) 
}

打印顺序:
after setState1 {number: 0}
after setState2 {number: 0}
setState1... {number: 0}
setState2... {number: 1}
render... {number: 2}
setState1 callback {number: 2}
setState2 callback {number: 2}

结论:

  • 状态是异步更新的
  • 可以发现在批量输出setState1... setState2... 后,即刻打印render...,最后打印 setState1 callback 以及 setState2 callback,这也正是说明setState的回调函数是在render更新之后执行的

setState接收函数的场景:setTimeout

handleClick = event => {
    setTimeout(() => {
        this.setState((prevState) => {
        console.log('setState1...', prevState)
        return { number: prevState.number + 1 }
        }, () => {
        console.log('setState1 callback', this.state)
        })

        console.log('after setState1', this.state) 

        this.setState((prevState) => {
        console.log('setState2...', prevState)
        return { number: prevState.number + 1 }
        }, () => {
        console.log('setState2 callback', this.state)
        })
        
        console.log('after setState2', this.state) 
    }, 4);
}

打印顺序:
setState1... {number: 0}
render... {number: 1}
setState1 callback {number: 1}
after setState1 {number: 1}
setState2... {number: 1}
render... {number: 2}
setState2 callback {number: 2}
after setState2 {number: 2}

结论

  • 状态是同步更新的
  • 一定要仔细品味setState、render、 setState callback、after setState打印顺序之间的关系!!!!

ReactDOM.unstable_batchedUpdates强制异步更新的场景

handleClick = event => {
    setTimeout(() => {
        ReactDOM.unstable_batchedUpdates(() => {
            this.setState((prevState) => {
                console.log('setState1...', prevState)
                return { number: prevState.number + 1 }
            }, () => {
                console.log('setState1 callback', this.state)
            })

            console.log('after setState1', this.state) 

            this.setState((prevState) => {
                console.log('setState2...', prevState)
                return { number: prevState.number + 1 }
            }, () => {
                console.log('setState2 callback', this.state)
            })

            console.log('after setState2', this.state) 
        })
    }, 4);
}

打印顺序:
after setState1 {number: 0}
after setState2 {number: 0}
setState1... {number: 0}
setState2... {number: 1}
render... {number: 2}
setState1 callback {number: 2}
setState2 callback {number: 2}

结论:

  • 在同步模式(legacy)下,如果需要在setTimeout等中启用异步更新,可以使用React17新增的ReactDOM.unstable_batchedUpdatesAPI

原理

如果需要了解setState同步异步更新的原理,请看下一篇文章