由實際問題探究setState的執行機制

一.幾個開發中經常會遇到的問題

以下幾個問題是我們在實際開發中經常會遇到的場景,下面用幾個簡單的示例代碼來還原一下。

1.setState是同步還是異步的,爲什麼有的時候不能立即拿到更新結果而有的時候可以?

1.1 鉤子函數和React合成事件中的setState

現在有兩個組件

  componentDidMount() {
    console.log('parent componentDidMount');
  }

  render() {
    return (
      <div>
        <SetState2></SetState2>
        <SetState></SetState>
      </div>
    );
  }

組件內部放入同樣的代碼,並在Setstate1中的componentDidMount中放入一段同步延時代碼,打印延時時間:

  componentWillUpdate() {
    console.log('componentWillUpdate');
  }

  componentDidUpdate() {
    console.log('componentDidUpdate');
  }

  componentDidMount() {
    console.log('SetState調用setState');
    this.setState({
      index: this.state.index + 1
    })
    console.log('state', this.state.index);
    
    console.log('SetState調用setState');
    this.setState({
      index: this.state.index + 1
    })
    console.log('state', this.state.index);
  }

下面是執行結果:

image

說明:

  • 1.調用setState不會立即更新
  • 2.所有組件使用的是同一套更新機制,當所有組件didmount後,父組件didmount,然後執行更新
  • 3.更新時會把每個組件的更新合併,每個組件只會觸發一次更新的生命週期。

1.2 異步函數和原生事件中的setstate

setTimeout中調用setState(例子和在瀏覽器原生事件以及接口回調中執行效果相同)

  componentDidMount() {
    setTimeout(() => {
      console.log('調用setState');
      this.setState({
        index: this.state.index + 1
      })
      console.log('state', this.state.index);
      console.log('調用setState');
      this.setState({
        index: this.state.index + 1
      })
      console.log('state', this.state.index);
    }, 0);
  }

執行結果:

image

說明:

  • 1.在父組件didmount後執行
  • 2.調用setState同步更新

2.爲什麼有時連續兩次setState只有一次生效?

分別執行以下代碼:

  componentDidMount() {
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
  }
  componentDidMount() {
    this.setState((preState) => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
    this.setState(preState => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
  }

執行結果:

1
1
2
2

說明:

  • 1.直接傳遞對象的setstate會被合併成一次
  • 2.使用函數傳遞state不會被合併

二.setState執行過程

由於源碼比較複雜,就不貼在這裏了,有興趣的可以去githubclone一份然後按照下面的流程圖去走一遍。

1.流程圖

setState

  • partialStatesetState傳入的第一個參數,對象或函數
  • _pendingStateQueue:當前組件等待執行更新的state隊列
  • isBatchingUpdates:react用於標識當前是否處於批量更新狀態,所有組件公用
  • dirtyComponent:當前所有處於待更新狀態的組件隊列
  • transcation:react的事務機制,在被事務調用的方法外包裝n個waper對象,並一次執行:waper.init、被調用方法、waper.close
  • FLUSH_BATCHED_UPDATES:用於執行更新的waper,只有一個close方法

2.執行過程

對照上面流程圖的文字說明,大概可分爲以下幾步:

  • 1.將setState傳入的partialState參數存儲在當前組件實例的state暫存隊列中。
  • 2.判斷當前React是否處於批量更新狀態,如果是,將當前組件加入待更新的組件隊列中。
  • 3.如果未處於批量更新狀態,將批量更新狀態標識設置爲true,用事務再次調用前一步方法,保證當前組件加入到了待更新組件隊列中。
  • 4.調用事務的waper方法,遍歷待更新組件隊列依次執行更新。
  • 5.執行生命週期componentWillReceiveProps
  • 6.將組件的state暫存隊列中的state進行合併,獲得最終要更新的state對象,並將隊列置爲空。
  • 7.執行生命週期componentShouldUpdate,根據返回值判斷是否要繼續更新。
  • 8.執行生命週期componentWillUpdate
  • 9.執行真正的更新,render
  • 10.執行生命週期componentDidUpdate

三.總結

1.鉤子函數和合成事件中:

react的生命週期和合成事件中,react仍然處於他的更新機制中,這時isBranchUpdate爲true。

按照上述過程,這時無論調用多少次setState,都會不會執行更新,而是將要更新的state存入_pendingStateQueue,將要更新的組件存入dirtyComponent

當上一次更新機制執行完畢,以生命週期爲例,所有組件,即最頂層組件didmount後會將isBranchUpdate設置爲false。這時將執行之前累積的setState

2.異步函數和原生事件中

由執行機制看,setState本身並不是異步的,而是如果在調用setState時,如果react正處於更新過程,當前更新會被暫存,等上一次更新執行後在執行,這個過程給人一種異步的假象。

在生命週期,根據JS的異步機制,會將異步函數先暫存,等所有同步代碼執行完畢後在執行,這時上一次更新過程已經執行完畢,isBranchUpdate被設置爲false,根據上面的流程,這時再調用setState即可立即執行更新,拿到更新結果。

3.partialState合併機制

我們看下流程中_processPendingState的代碼,這個函數是用來合併state暫存隊列的,最後返回一個合併後的state


  _processPendingState: function (props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;

    if (!queue) {
      return inst.state;
    }

    if (replace && queue.length === 1) {
      return queue[0];
    }

    var nextState = _assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
    }

    return nextState;
  },

我們只需要關注下面這段代碼:

 _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);

如果傳入的是對象,很明顯會被合併成一次:

Object.assign(
  nextState,
  {index: state.index+ 1},
  {index: state.index+ 1}
)

如果傳入的是函數,函數的參數preState是前一次合併後的結果,所以計算結果是準確的。

4.componentDidMount調用setstate

在componentDidMount()中,你 可以立即調用setState()。它將會觸發一次額外的渲染,但是它將在瀏覽器刷新屏幕之前發生。這保證了在此情況下即使render()將會調用兩次,用戶也不會看到中間狀態。謹慎使用這一模式,因爲它常導致性能問題。在大多數情況下,你可以 在constructor()中使用賦值初始狀態來代替。然而,有些情況下必須這樣,比如像模態框和工具提示框。這時,你需要先測量這些DOM節點,才能渲染依賴尺寸或者位置的某些東西。

以上是官方文檔的說明,不推薦直接在componentDidMount直接調用setState,由上面的分析:componentDidMount本身處於一次更新中,我們又調用了一次setState,就會在未來再進行一次render,造成不必要的性能浪費,大多數情況可以設置初始值來搞定。

當然在componentDidMount我們可以調用接口,再回調中去修改state,這是正確的做法。

當state初始值依賴dom屬性時,在componentDidMountsetState是無法避免的。

5.componentWillUpdate componentDidUpdate

這兩個生命週期中不能調用setState

由上面的流程圖很容易發現,在它們裏面調用setState會造成死循環,導致程序崩潰。

6.推薦使用方式

在調用setState時使用函數傳遞state值,在回調函數中獲取最新更新後的state

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章