對React setState的一些思考與心得

mpsky發表於2021-09-09

前言

這篇文章主要是為了紀錄一些自己對於setState的認識的不斷深入的過程。我覺得這過程對我自己來說很有價值,不光是層層遞進的瞭解一個API的執行機制,更對react總體的設計有了更為深入的認識。

第一階段 初識setState

使用過React的應該都知道,在React中,一個元件中要讀取當前狀態需要訪問this.state,但是更新狀態卻需要使用this.setState,不是直接在this.state上修改,就比如這樣:

//讀取狀態const count = this.state.count;//更新狀態this.setState({count: count + 1});//無意義的修改this.state.count = count + 1;

其實這主要有幾點考慮,首先this.state說到底只是一個物件,單純的去修改一個物件的值是毫無意義的,在React中只有去驅動UI的更新才會有意義,因此雖然我們可以嘗試直接改變this.state,但並沒有驅動UI的重新渲染,因此這種操作也就毫無意義。也正是由於這個原因,我們就需要使用this.setState來驅動元件的更新過程。

然後在我剛學習React時,我就看見了這段很經典的程式碼:

function incrementMultiple() {  this.setState({count: this.state.count + 1});  this.setState({count: this.state.count + 1});  this.setState({count: this.state.count + 1});
}

作為一名JSer,我看完就毫不猶豫的想到,這特麼不就是count的值加3麼,但轉眼看了下面的答案,光速打臉,實際的結果是state只增加了1。然後我就不由想到當時沒怎麼看懂的React文件中的一些話:狀態更新可能是非同步的,狀態更新合併。恩,沒毛病,因為非同步且會合並,因此這三條語句合併為一條語句了,所以就只執行一次。然後就扭頭溜了,並沒有去思考一些深層次的問題。

第二階段 setState理解的進階

但是隨著對React的理解的逐步加深,我開始對setState有了更加深的理解:

首先我意識到this.setState會透過引發一次元件的更新過程來引發重新繪製。也就是說setState的呼叫會引起React的更新生命週期的四個函式的依次呼叫:

  • shouldComponentUpdate

  • componentWillUpdate

  • render

  • componentDidUpdate

我們都知道,在React生命週期函式里,以render函式為界,無論是掛載過程和更新過程,在render之前的幾個生命週期函式,this.state和Props都是不會發生更新的,直到render函式執行完畢後,this.state才會得到更新。(有一個例外:當shouldComponentUpdate函式返回false,這時候更新過程就被中斷了,render函式也不會被呼叫了,這時候React不會放棄掉對this.state的更新的,所以雖然不呼叫render,依然會更新this.state。)

React的官方文件有提到過這麼一句話:

狀態更新會合並(也就是說多次setstate函式呼叫產生的效果會合並)。

起初我對這句話理解並不是很深刻,但按照官方文件的程式碼示例寫了這麼一段程式碼:

function updateName() {  this.setState({Age: '22'})  this.setState({Name: 'srtian'})
}

果然執行結果與以下程式碼是等價的

function updateName() {
  this.setState({Age: '22', Name: 'srtian})
}

於是我將其理解為一個佇列,每個this.setState()都會被合併起來,排成一排,到最後一次解決。但對其設計的原因並不理解,只知道這樣有利於效能(也是在文件上看到的)。

直到理解上面React生命週期函式的原理後,我才理解了setState關於這個設計的意圖。

前面我們提到過,每一次使用setState都會呼叫一次更新的生命週期,如果每一次this.serState()都呼叫一次上面那四個生命週期函式,雖然以上四個函式都是純函式,效能浪費上還好,但render函式會將結果拿去做Virtual DOM比較和更新DOM樹,這個就比較費時間。因此,將多個this.setSate進行合併,render函式就能夠將合併後的this.setState()的結果一次性的與Virtual DOM比較然後更新DOM樹,這樣就能夠用有效的提升效能。

除此之外,我還認為setState的設計十分巧妙,一般來說只在render函式後才會進行更新this.state。這其實也避免了React16的Fiber可能會產生的一個問題:由於Fiber下的元件更新是可以中斷,也就是說在一個元件的更新過程中,可能更新到一半的時候就由於其他原因而中斷更新,回去做更重要的事情了,在做完更重要的事情後,再回來更新這個元件,這會導致前面的那些生命週期函式可能會執行多次。因此如果在render之前this.setState()就改變狀態的話,很有可能就會導致元件狀態的多次更新,從而導致元件狀態的混亂。

第三階段 從原始碼理解setstate

這是React15.6版本,由於React16變動較大,setState的呼叫棧發生變動,因此僅供參考。

經歷了上面那個階段,我算是對setState有那麼一些理解了,但還是不能理解很多東西比如:this.setState()的是怎麼合併的?setState()到底是怎樣一種騷操作?...等等。然後我又看見了這段經典的程式碼:

class Example extends React.Component {  constructor() {    super();    this.state = {      val: 0
    };
  }
  
  componentDidMount() {    this.setState({val: this.state.val + 1});    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {      this.setState({val: this.state.val + 1});      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {    return null;
  }
};

恩!按照我多年經驗,這波操作我看不懂!

圖片描述

image

於是硬著頭皮開啟了React原始碼,開始一波瞎分析:
首先就是setState了,可以看出它接受兩個引數partialState和callback,其中partialState顧名思義就是部分state,起這個名字也能就是想表達它的state沒有改變(瞎猜的。。。)。以下是省略了一部分的程式碼,只看核心部分。

ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(    typeof partialState === 'object' ||      typeof partialState === 'function' ||
      partialState == null,    'setState(...): takes an object of state variables to update or a ' +      'function which returns an object of state variables.',
  );  this.updater.enqueueSetState(this, partialState);  if (callback) {    this.updater.enqueueCallback(this, callback, 'setState');
  }
};
 enqueueSetState: function(publicInstance, partialState) {    if (__DEV__) {
      ReactInstrumentation.debugTool.onSetState();
      warning(
        partialState != null,        'setState(...): You passed an undefined or null state object; ' +          'instead, use forceUpdate().',
      );
    }    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,      'setState',
    );    if (!internalInstance) {      return;
    }    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  }// 透過enqueueUpdate執行state更新function enqueueUpdate(component) {
  ensureInjected();  // batchingStrategy是批次更新策略,isBatchingUpdates表示是否處於批次更新過程
  // 最開始預設值為false
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);    return;
  }
  dirtyComponents.push(component);  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}// 對_pendingElement, _pendingStateQueue, _pendingForceUpdate進行判斷,// _pendingStateQueue由於會對state進行修改,所以不為空,// 然後會呼叫updateComponent方法performUpdateIfNecessary: function(transaction) {    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(        this,        this._pendingElement,
        transaction,        this._context,
      );
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {      this.updateComponent(
        transaction,        this._currentElement,        this._currentElement,        this._context,        this._context,
      );
    } else {      this._updateBatchNumber = null;
    }
  },

其中這段程式碼需要額外注意:

  // batchingStrategy是批次更新策略,isBatchingUpdates表示是否處於批次更新過程
  // 最開始預設值為falseif (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);    return;
  }
dirtyComponents.push(component);if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }

上面這段程式碼的意思就是如果是處於批次更新模式,也就是isBatchingUpdates為true時,不進行state的更新操作,而是將需要更新的component新增到dirtyComponents陣列中。
如果不處於批次更新模式,則對所有佇列中的更新執行batchedUpdates方法。

然後可以找到了這個batchedUpdates:

var ReactDefaultBatchingStrategy = {  // 也就是上面提到的預設為false
  isBatchingUpdates: false,  // 這個方法只有在isBatchingUpdates: false時才會呼叫
  // 但一般來說,處於react大事務中時,會在render中的_renderNewRootComponent中將其設定為true。
  batchedUpdates: function(callback, a, b, c, d, e) {    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {      return callback(a, b, c, d, e);
    } else {      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },

看到這我總算理解了,當我們呼叫setState時,最終會透過enqueueUpdate執行state更新,就像上面那樣有兩種更新的模式,一種是批次更新模式,將組建儲存在dirtyComponents;另一種非批次模式,將會遍歷dirtyComponents,對每一個dirtyComponents呼叫updateComponent方法。就像這張圖:

圖片描述

流程圖

至於批次與非批次模式,會透過ReactDefaultBatchingStrategy中的isBatchingUpdates屬性來進行判斷。在非批次模式下,會立即應用新的state;而在批次模式下,需要更新state的元件會被push 到dirtyComponents,再執行更新。

所以我們再看前面的那坨程式碼:

class Example extends React.Component {  constructor() {    super();    this.state = {      val: 0
    };
  }
  
  componentDidMount() {    this.setState({val: this.state.val + 1});    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {      this.setState({val: this.state.val + 1});      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {    return null;
  }
};

就不難看出它的答案是 0, 0, 2, 3。

總結起來就是這樣:

  • this.setState首先會把state推入pendingState佇列中

  • 然後將元件標記為dirty

  • React中有事務的概念,最常見的就是更新事務,如果不在事務中,則會開啟一次新的更新事務,更新事務執行的操作就是把元件標記為dirty。

  • 判斷是否處於batch update

  • 是的話,儲存組建於dirtyComponent中,在事務結束的時候才會透過 ReactUpdates.flushBatchedUpdates 方法將所有的臨時 state merge 並計算出最新的 props 及 state,然後將其批次執行,最後再關閉結束事務。

  • 不是的話,直接開啟一次新的更新事務,在標記為dirty之後,直接開始更新元件。因此當setState執行完畢後,元件就更新完畢了,所以會造成定時器同步更新的情況。

另外還有就是updateComponent方法,這也很重要:

{   // 會檢測元件中的state和props是否發生變化,有變化才會進行更新; 
    // 如果shouldUpdateComponent函式中返回false則不會執行元件的更新
    updateComponent: function (transaction,
                               prevParentElement,
                               nextParentElement,
                               prevUnmaskedContext,
                               nextUnmaskedContext,) {        var inst = this._instance;        var nextState = this._processPendingState(nextProps, nextContext);        var shouldUpdate = true;        if (!this._pendingForceUpdate) {            if (inst.shouldComponentUpdate) {                if (__DEV__) {
                    shouldUpdate = measureLifeCyclePerf(                            () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),                            this._debugID,                            'shouldComponentUpdate',
                    );
                } else {
                    shouldUpdate = inst.shouldComponentUpdate(
                            nextProps,
                            nextState,
                            nextContext,
                    );
                }
            } else {                if (this._compositeType === CompositeTypes.PureClass) {
                    shouldUpdate =
                            !shallowEqual(prevProps, nextProps) ||
                            !shallowEqual(inst.state, nextState);
                }
            }
        }
        
    },// 該方法會合並需要更新的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 = Object.assign({}, replace ? queue[0] : inst.state);        for (var i = replace ? 1 : 0; i 

發現它會呼叫shouldComponentUpdate和componentWillUpdate方法,看到這不由理解了一個定律:不要在shouldComponentUpdate和componentWillUpdate中呼叫setState。如果在這兩個生命週期裡呼叫setState,會造成造成迴圈呼叫。



作者:Srtian
連結:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1806/viewspace-2810092/,如需轉載,請註明出處,否則將追究法律責任。

相關文章