使用React.setState須要注意的三點

原文: https://medium.com/@mweststra...
做者: Michel Weststratejavascript


前言

這篇文章原標題是3 Reasons why I stopped using React.setState,可是我對原文做者提出的論點不是很感冒,可是做者提出的三點對React新手來講是很容易忽略的地方,因此我在這裏只提出部份內容,並且把標題改成使用React.setState須要注意的三點html

正文

React新手來講,使用setState是一件很複雜的事情。即便是熟練的React開發,也頗有可能由於React的一些機制而產生一些bug,好比下面這個例子:java

因爲異步的setState形成的bug。log永遠比當前的慢一步

文檔中也說明了當使用setState的時候,須要注意什麼問題:react

注意:
絕對不要 直接改變this.state,由於以後調用setState()可能會替換掉你作的改變。把this.state當作是不可變的。git

setState()不會馬上改變this.state,而是建立一個即將處理的state轉變。在調用該方法以後訪問this.state可能會返回現有的值。github

setState的調用沒有任何同步性的保證,而且調用可能會爲了性能收益批量執行。app

setState()將老是觸發一次重繪,除非在shouldComponentUpdate()中實現了條件渲染邏輯。若是可變對象被使用了,但又不能在shouldComponentUpdate()中實現這種邏輯,僅在新state和以前的state存在差別的時候調用setState()能夠避免沒必要要的從新渲染。異步

總結出來,當使用setState的時候,有三個問題須要注意:函數

1. setState是異步的(譯者注:不保證同步的)

不少開發剛開始沒有注意到setState是異步的。若是你修改一些state,而後直接查看它,你會看到以前的state。這是setState中最容易出錯的地方。 setState這個詞看起來並不像是異步的,因此若是你不假思索的用它,可能會形成bugs。下面這個例子很好的展現了這個問題:性能

class Select extends React.Component {
  constructor(props, context) {
    super(props, context)
    this.state = {
      selection: props.values[0]
    };
  }
  
  render() {
    return (
      <ul onKeyDown={this.onKeyDown} tabIndex={0}>
        {this.props.values.map(value =>
          <li
            className={value === this.state.selection ? 'selected' : ''}
            key={value}
            onClick={() => this.onSelect(value)}
          >
            {value}
          </li> 
        )}  
      </ul>
    )
  }
  
  onSelect(value) {
    this.setState({
      selection: value
    })
    this.fireOnSelect()
  }

  onKeyDown = (e) => {
    const {values} = this.props
    const idx = values.indexOf(this.state.selection)
    if (e.keyCode === 38 && idx > 0) { /* up */
      this.setState({
        selection: values[idx - 1]
      })
    } else if (e.keyCode === 40 && idx < values.length -1) { /* down */
      this.setState({
        selection: values[idx + 1]
      })  
    }
    this.fireOnSelect()
  }
   
  fireOnSelect() {
    if (typeof this.props.onSelect === "function")
      this.props.onSelect(this.state.selection) /* not what you expected..*/
  }
}

ReactDOM.render(
  <Select 
    values={["State.", "Should.", "Be.", "Synchronous."]} 
    onSelect={value => console.log(value)}
  />,
  document.getElementById("app")
)

第一眼看上去,這個代碼彷佛沒有什麼問題。兩個事件處理中調用onSelect方法。可是,這個Select組件中有一個bug很好的展示了以前的GIF圖。onSelect方法永遠傳遞的是以前的state.selection值,由於當fireOnSelect調用的時候,setState尚未完成它的工做。我認爲React至少要把setState更名爲scheduleState或者把回掉函數設爲必須參數。

這個bug很容易修改,最難的地方在於你要知道有這個問題。

2. setState會形成沒必要要的渲染

setState形成的第二個問題是:每次調用都會形成從新渲染。不少時候,這些從新渲染是沒必要要的。你能夠用React performance tools中的printWasted來查看何時會發生沒必要要渲染。可是,大概的說,沒必要要的渲染有如下幾個緣由:

  • 新的state其實和以前的是同樣的。這個問題一般能夠經過shouldComponentUpdate來解決。也能夠用pure render或者其餘的庫來解決這個問題。

  • 一般發生改變的state是和渲染有關的,可是也有例外。好比,有些數據是根據某些狀態來顯示的。

  • 第三,有些state和渲染一點關係都沒有。有一些state多是和事件、timer ID有關的。

3.setState並不能頗有效的管理全部的組件狀態

基於上面的最後一條,並非全部的組件狀態都應該用setState來進行保存和更新的。複雜的組件可能會有各類各樣的狀態須要管理。用setState來管理這些狀態不但會形成不少不須要的從新渲染,也會形成相關的生命週期鉤子一直被調用,從而形成不少奇怪的問題。

後話

在原文中做者推薦了一個叫作MobX的庫來管理部分狀態,我不是很感冒,因此我就不介紹。若是感興趣的,能夠經過最上面的連接看看原文中的介紹。

基於上面提出的三點,我認爲新手應該注意的地方是:

setState是不保證同步的

setState是不保證同步的,是不保證同步的,是不保證同步的。重要的事情說三遍。之因此不說它是異步的,是由於setState在某些狀況下也是同步更新的。能夠參考這篇文章

若是須要在setState後直接獲取修改後的值,那麼有幾個方案:

傳入對應的參數,不經過this.state獲取

針對於以前的例子,徹底能夠在調用fireOnSelect的時候,傳入須要的值。而不是在方法中在經過this.state來獲取

使用回調函數

setState方法接收一個function做爲回調函數。這個回掉函數會在setState完成之後直接調用,這樣就能夠獲取最新的state。對於以前的例子,就能夠這樣:

this.setState({
  selection: value
}, this.fireOnSelect)

使用setTimeout

setState使用setTimeout來讓setState先完成之後再執行裏面內容。這樣子:

this.setState({
  selection: value
});

setTimeout(this.fireOnSelect, 0);

直接輸出,回調函數,setTimeout對比

componentDidMount(){
    this.setState({val: this.state.val + 1}, ()=>{
      console.log("In callback " + this.state.val);
    });

    console.log("Direct call " + this.state.val);   

    setTimeout(()=>{
      console.log("begin of setTimeout" + this.state.val);

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

      setTimeout(()=>{
        console.log("setTimeout of settimeout " + this.state.val);
      }, 0);

      console.log("end of setTimeout " + this.state.val);
    }, 0);
  }

若是val默認爲0, 輸入的結果是:

> Direct call 0
> In callback 1
> begin of setTimeout 1
> setTimeout setState callback 2
> end of setTimeout 2
> setTimeout of settimeout 2

和渲染無關的狀態儘可能不要放在state中來管理

一般state中只來管理和渲染有關的狀態,從而保證setState改變的狀態都是和渲染有關的狀態。這樣子就能夠避免沒必要要的重複渲染。其餘和渲染無關的狀態,能夠直接以屬性的形式保存在組件中,在須要的時候調用和改變,不會形成渲染。或者參考原文中的MobX

避免沒必要要的修改,當state的值沒有發生改變的時候,儘可能不要使用setState。雖然shouldComponentUpdatePureComponent能夠避免沒必要要的重複渲染,可是仍是增長了一層shallowEqual的調用,形成多餘的浪費。

以上

相關文章
相關標籤/搜索