1. 程式人生 > >深入React技術棧之setState詳解

深入React技術棧之setState詳解

丟擲問題


class Example extends Component {
  contructor () {
    super()
    this.state = {
      value: 0,
      index: 0
    }
  }

  componentDidMount () {
    this.setState({value: this.state.value + 1})
    console.log(this.state.value) // 第一次輸出
    this.setState({value: this.state.value + 1})
    console.log(this.state.value) // 第二次輸出
    setTimeout(() => {
      this.setState({value: this.state.value + 1})
      console.log(this.state.value) // 第三次輸出
      this.setState({value: this.state.value + 1})
      console.log(this.state.value) // 第四次輸出
    }, 0);
        this.refs.button.addEventListener('click', this.click)
  }

  click = () => {
    this.setState({value: this.state.index + 1})
    this.setState({value: this.state.index + 1})
  }

  render () {
    return (
      <div><span>value: {this.state.value}index: {this.props.index}</span>
        <button ref="button" onClick={this.click}>點選</button>
      </div>
    )
  }
}
  • 這四次輸出,按常理來說分別是: 1,2,3,4。但是,實際輸出為: 0, 0, 2, 3

setState的注意點

  1. setState不會立刻改變React元件中state的值(即setState是非同步更新)

    • setState通過一個佇列機制實現state更新;
    • 當執行setState時,會將需要更新的state合併後放入狀態佇列,而不會立即更新,佇列可以高效的批量更新state;
    • 通過this.state直接修改的值,state不會放入狀態佇列,當下次呼叫setState並對狀態佇列進行合併時,會忽略之前直接被修改的state.
  2. setState通過引發一次元件的更新過程來引發重新繪製

    • 此處重繪指的就是引起React的更新生命週期函式4個函式:
    • shouldComponentUpdate(被呼叫時this.state沒有更新;如果返回了false,生命週期被中斷,雖然不呼叫之後的函數了,但是state仍然會被更新)
    • componentWillUpdate(被呼叫時this.state沒有更新)
    • render(被呼叫時this.state得到更新)
    • componentDidUpdate
  3. 多個相鄰的state的修改可能會合併到一起一次執行

 this.setState({name: 'Pororo'})
 this.setState({age: 20})
  • 等同於

 this.setState({name: 'Pororo',age: 20})
  • 上面兩塊程式碼的效果是一樣的。如果每次呼叫都引發一次生命週期更新,那效能就會消耗很大了。所以,React會將多個this.setState產生的修改放進一個佇列裡,等差不多的時候就會引發一次生命週期更新。

問題分析

  • 對於前兩次setState:

this.setState({value: this.state.val + 1});
console.log(this.state.value); // 第一次輸出
this.setState({value: this.state.val + 1});
console.log(this.state.value); // 第二次輸出
  • 由於setState不會立即改變React元件中state的值,所以兩次setState中this.state.value都是同一個值0,故而,這兩次輸出都是0。因而value只被加1。
  • 既然這樣,那麼是不是可以直接操作this.state呢?比如:this.state.value=this.state.value+1;
  • 這樣的確可以修改this.state.value的狀態但是卻不可以引發重複渲染。
  • 所以,就必須通過React設定的setState函式去改變this.state,從而引發重新渲染。
  • setTimeout裡面的兩次setState:

setTimeout(() => {
  this.setState({value: this.state.value + 1})
  console.log(this.state.value) // 第三次輸出
  this.setState({value: this.state.value + 1})
  console.log(this.state.value) // 第四次輸出
}, 0);
  • 這兩次this.state的值同步更新了;
  • 同步更新:是由React引發的事件處理(比如:onClick引發的事件處理),呼叫setState會非同步更新this.state;
  • 非同步更新:除此之外的setState呼叫會同步執行this.setState。 “除此之外”指的是:繞過React通過addEventListener直接新增的事件處理函式和setTimeout/setInterval產生的非同步呼叫。
  • this.setState更新機制圖解:

  • 每次setState產生新的state會依次被存入一個佇列,然後會根據isBathingUpdates變數判斷是直接更新this.state還是放進dirtyComponent裡回頭再說。
  • isBatchingUpdates預設是false,也就表示setState會同步更新this.state。
  • 但是,當React在呼叫事件處理函式之前就會呼叫batchedUpdates,這個函式會把isBatchingUpdates修改為true,造成的後果就是由React控制的事件處理過程setState不會同步更新this.state。

同步更新(函式式setState)

  1. 如果this.setState的引數不是一個物件而是一個函式時,這個函式會接收到兩個引數,第一個是當前的state值,第二個是當前的props,這個函式應該返回一個物件,這個物件代表想要對this.state的更改;
  2. 換句話說,之前你想給this.setState傳遞什麼物件引數,在這種函式裡就返回什麼物件。不過,計算這個物件的方法有些改變,不再依賴於this.state,而是依賴於輸入引數state。

function increment(state, props) {
  return {count: state.count + 1};
}

function incrementMultiple() {
  this.setState(increment);
  this.setState(increment);
  this.setState(increment);
}
  • 假如當前this.state.count的值是0,第一次呼叫this.setState(increment),傳給increment的state引數是0,第二呼叫時,state引數是1,第三次呼叫是,引數是2,最終incrementMultiple讓this.state.count變成了3。
  • 對於多次呼叫函式式setState的情況,React會保證呼叫每次increment時,state都已經合併了之前的狀態修改結果。
要注意的是,在increment函式被呼叫時,this.state並沒有被改變,依然,要等到render函式被重新執行時(或者shouldComponentUpdate函式返回false之後)才被改變。

同步非同步setState的用法混合


function incrementMultiple() {
  this.setState(increment);
  this.setState(increment);
  this.setState({count: this.state.count + 1});
  this.setState(increment);
}
  • 在幾個函式式setState呼叫中插入一個傳統式setState呼叫,最後得到的結果是讓this.state.count增加了2,而不是增加4。
  • 這是因為React會依次合併所有setState產生的效果,雖然前兩個函式式setState呼叫產生的效果是count加2,但是中間出現一個傳統式setState呼叫,一下子強行把積攢的效果清空,用count加1取代。
  • 所以,傳統式setState與函式式setState一定不要混用。

總結自:掘金(不洗碗工作室)

原文地址:https://segmentfault.com/a/1190000014990454