React setState真的是非同步的嗎?

React中的setState更新主要可以分為四種情況:

  1. 合成事件中的setState

  2. 生命週期中的setState

  3. 原生事件中的setState

  4. setTimeout中的setState

一、 合成事件中的setState

合成事件指的是react為了解決跨平臺,相容性等問題,在原始碼中封裝的一套事件機制,與原生事件類似,扮演了原生事件的作用,像在jsx中的onClick、onChange這些都是原生事件。

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新前的val --> 0
  }
  render() {
    return (
      <div onClick={this.increment}>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

這裡原始碼沒看懂,因為進到了某個判斷分支中,return出來了,所以還沒更新就執行了console.log,呈現出“非同步”的效果。

二、生命週期中的setState

生命週期是react中封裝的一些周期函式,如componentDidMount等。

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 輸出的還是更新前的值 --> 0
 }
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

跟合成事件一樣,當componentDidMount執行的時候,react內部並沒有更新,執行完 componentDidMount 後才去

commitUpdateQueue 更新,這就導致在 componentDidMountsetState 完去 console.log 拿的結果還是更新前的值。

三、原生事件中的setState

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新後的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

原生事件指的是非react合成事件,如自帶的事件監聽 addEventListener 等原生繫結的事件。原生事件沒有走合成事件的判斷分支,直接觸發click事件,不像合成事件或者鉤子函式被return,所以在原生事件中 setState 後能同步拿到最新值。

四、 setTimeout中的setState

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 輸出更新後的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

setTimeout並不是一個獨立的場景,它是隨著你外層決定的,你可以在合成事件中setTimeout,可以在鉤子函式中setTimeout,也可以在原生事件中setTimeout,不管在哪個場景下,setTimeout中裡去setState都可以拿到最新的state值。表現得跟原生事件一樣。

一個例子

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

結合上面分析的,鉤子函式中的 setState 無法立馬拿到更新後的值,所以前兩次都是輸出0,當執行到 setTimeout 裡的時候,前面兩個state的值已經被更新,由於 setState 批量更新的策略, this.state.val 只對最後一次的生效,為1,而在 setTimmoutsetState 是可以同步拿到更新結果,所以 setTimeout 中的兩次輸出2,3,最終結果就為 0, 0, 2, 3