前面提到事務即將結束時,會去調用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的一個地方,要避免這個問題,需要對組件生命周期有一定的了解。
