關於React setState的實現原理(三)


前面提到事務即將結束時,會去調用FLUSH_BATCHED_UPDATES的flushBatchedUpdates方法執行批量更新,該方法會去遍歷dirtyComponents,對每一項執行performUpdateIfNecessary方法,該方法代碼如下:

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;
    }
  }

 

在我們的setState更新中,其實只會用到第二個 this._pendingStateQueue !== null 的判斷,即如果_pendingStateQueue中還存在未處理的state,那就會執行updateComponent完成更新。那_pendingStateQueue是何時被處理的呢,繼續看!

通過翻閱updateComponent方法,我們可以知道_pendingStateQueue是在該方法中由_processPendingState(nextProps, nextContext)方法做了一些處理,該方法傳入兩個參數,新的props屬性和新的上下文環境,這個上下文環境可以先不用管。我們看看_processPendingState的具體實現:

_processPendingState: function (props, context) {
    var inst = this._instance;    // _instance保存了Constructor的實例,即通過ReactClass創建的組件的實例
    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;
  },

 

什么replace啊什么的都可以暫時不用看,主要先看for循環內部做的事情,replace我們暫時認為是false。for循環遍歷了_pendingStateQueue中所有保存的狀態,對於每一個狀態進行處理,處理時首先判斷保存的是function還是object。若是function,就在inst的上下文中執行該匿名函數,該函數返回一個代表新state的object,然后執行assign將其與原有的state合並;若是object,則直接與state合並。注意,傳入setState的第一個參數如果是function類型,我們可以看到,其第一個參數nextState即表示更新之前的狀態;第二個參數props代表更新之后的props,第三個context代表新的上下文環境。之后返回合並后的state。這里還需要注意一點,這一點很關鍵,代碼中出現了this._pendingStateQueue = null這么一段,這也就意味着dirtyComponents進入下一次循環時,執行performUpdateIfNecessary不會再去更新組件,這就實現了批量更新,即只做一次更新操作,React在更新組件時就是用這種方式做了優化。

好了,回來看我們的案例,當我們傳入函數作為setState的第一個參數時,我們用該函數提供給我們的state參數來訪問組件的state。該state在代碼中就對應nextState這個值,這個值在每一次for循環執行時都會對其進行合並,因此第二次執行setState,我們在函數中訪問的state就是第一次執行setState后已經合並過的值,所以會打印出2。然而直接通過this.state.count來訪問,因為在執行對_pendingStateQueue的for循環時,組件的update還未執行完,this.state還未被賦予新的值,其實了解一下updateComponent會發現,this.state的更新會在_processPendingState執行完執行。所以兩次setState取到的都是this.state.count最初的值0,這就解釋了之前的現象。其實,這也是React為了解決這種前后state依賴但是state又沒及時更新的一種方案,因此在使用時大家要根據實際情況來判斷該用哪種方式傳參。

接下來我們再來看看setState的第二個參數,回調函數,它是在什么時候執行的。

class Root extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    componentDidMount() {
        let me = this;
        setTimeout(function() {
            me.setState({count: me.state.count + 1}, function() {
                console.log('did callback');
            });
            console.log('hello');
        }, 0);
    }

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

    render() {
        return <h1>{this.state.count}</h1>
    }
}

 

這個案例控制台打印順序是怎樣的呢?不賣關子了,答案是did update,did callback,hello。這里是在一個setTimeout中執行了setState,因此其處於一個單獨的事務之中,所以hello最后打印容易理解。然后我們來看看setState執行更新時做了些啥。前面我們知道在執行完組件裝載即調用了componentDidMount之后,事務開始執行一系列close方法,這其中包括調用FLUSH_BATCHED_UPDATES中的flushBatchedUpdates,我們來看看這段代碼。

var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
  // array and perform any updates enqueued by mount-ready handlers (i.e.,
  // componentDidUpdate) but we need to check here too in order to catch
  // updates enqueued by setState callbacks and asap calls.
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);    // 處理批量更新
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();    // 處理callback
      CallbackQueue.release(queue);
    }
  }
};

 

可以看我做了中文標注的兩個地方,這個方法其實主要就是處理了組件的更新和callback的調用。組件的更新發生在runBatchedUpdates這個方法中,下面的queue.notifyAll內部其實就是從隊列中去除callback調用,因此應該是先執行完更新,調用componentDidUpdate方法之后,再去執行callback,就有了我們上面的結果。

總結

React在組件更新方面做了很多優化,這其中就包括了上述的批量更新。在componentDidMount中執行了N個setState,如果執行N次更新是件很傻的事情。React利用其獨特的事務實現,做了這些優化。正是因為這些優化,才造成了上面見到的怪現象。還有一點,再使用this.state時一定要注意組件的生命周期,很多時候在獲取state的時候,組件更新還未完成,this.state還未改變,這是很容易造成bug的一個地方,要避免這個問題,需要對組件生命周期有一定的了解。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM