深刻研究React setState的工做機制

前言

上個月發表了一篇 React源碼學習——ReactClass,可是後來我發現,你們對這種大量貼代碼分析源碼的形式並不感冒。講道理,我本身看着也煩,還不如本身直接去翻源碼來得痛快。吸收了上一次的教訓,此次我決定:理性貼代碼!翻閱源代碼的工做仍是留給各位小夥伴本身去作比較好。原本此次想準備說一說咱們平時一直提到的React Virture DOM,但這可能又會形成無限貼源碼的後果,由於virture dom在React中主要就是一個對象,在ReactElement中定義的,感興趣的同窗去源碼中搜索一下createElement方法,就能看到virture dom是啥東西了。對其自己是沒啥好說的,須要分析的應該是其在組件掛載和更新時的應用,所以對於ReactElement自己就不單獨拿出來說了,你們感興趣就去翻閱一下源碼吧。html

進入正題

此次主要是要分析一下React中常見的setState方法,熟悉React的小夥伴應該都知道,該方法一般用於改變組件狀態並用新的state去更新組件。可是,這個方法在不少地方的表現老是與咱們的預期不符,先來看幾個案例。sql

案例一

 

 1 class Root extends React.Component {
 2   constructor(props) {
 3     super(props);
 4     this.state = {
 5       count: 0
 6     };
 7   }
 8   componentDidMount() {
 9     let me = this;
10     me.setState({
11       count: me.state.count + 1
12     });
13     console.log(me.state.count);    // 打印出0
14     me.setState({
15       count: me.state.count + 1
16     });
17     console.log(me.state.count);    // 打印出0
18     setTimeout(function(){
19      me.setState({
20        count: me.state.count + 1
21      });
22      console.log(me.state.count);   // 打印出2
23     }, 0);
24     setTimeout(function(){
25      me.setState({
26        count: me.state.count + 1
27      });
28      console.log(me.state.count);   // 打印出3
29     }, 0);
30   }
31   render() {
32     return (
33       <h1>{this.state.count}</h1>
34     )
35   }
36 }

 

這個案例你們可能在別的地方中也見到過,結果確實讓人匪夷所思,打印出0,0,2,3。先拋出兩個問題:數組

  1. 爲何不在setTimeout中執行的兩次setState均打印出0?
  2. 爲何setTimeout中執行的兩次setState會打印出不一樣結果?

帶着兩個問題往下看。app

React中的transaction(事務)

說到事務,我第一反應就是在之前使用sql server時用來處理批量操做的一個機制。當全部操做均執行成功,便可以commit transaction;如有一個操做失敗,則執行rollback。在React中,也實現了一種相似的事務機制,其餘文章也有詳細的介紹。按照我我的的理解,React中一個事務其實就是按順序調用一系列函數。在React中就是調用perform方法進入一個事務,該方法中會傳入一個method參數。執行perform時先執行initializeAll方法按順序執行一系列initialize的操做,例如一些初始化操做等等,而後執行傳入的method,method執行完後就執行closeAll方法按順序執行一系列close操做,例以下面會提到的執行批量更新或者將isBatchingUpdates變回false等等,而後結束此次事務。React中內置了不少種事務,注意,同一種事務不能同時開啓,不然會拋出異常。咱們仍是回到咱們上面的案例中來講明這個過程。dom

組件在調用ReactDOM.render()以後,會執行一個_renderNewRootComponent方法,你們能夠去翻閱源碼看一看,該方法執行了一個ReactUpdates.batchedUpdates()。batchedUpdates是什麼呢?咱們看看它的代碼。ide

 1 var transaction = new ReactDefaultBatchingStrategyTransaction();
 2 
 3 var ReactDefaultBatchingStrategy = {
 4   isBatchingUpdates: false,
 5 
 6   /**
 7    * Call the provided function in a context within which calls to `setState`
 8    * and friends are batched such that components aren't updated unnecessarily.
 9    */
10   batchedUpdates: function (callback, a, b, c, d, e) {
11     var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
12 
13     ReactDefaultBatchingStrategy.isBatchingUpdates = true;
14 
15     // The code is written this way to avoid extra allocations
16     if (alreadyBatchingUpdates) {
17       return callback(a, b, c, d, e);
18     } else {
19       return transaction.perform(callback, null, a, b, c, d, e);
20     }
21   }
22 };

 

從代碼中咱們能夠看出,這個batchedUpdates因爲是第一次被調用,alreadyBatchingUpdates爲false,所以會去執行transaction.perform(method),這就將進入一個事務,這個事務具體作了啥咱們暫時不用管,咱們只須要知道這個transaction是ReactDefaultBatchingStrategyTransaction的實例,它表明了其中一類事務的執行。而後會在該事務中調用perform中傳入的method方法,即開啓了組件的首次裝載。當裝載完畢會調用componentDidMount(注意,此時仍是在執行method方法,事務還沒結束,事務只有在執行完method後執行一系列close纔會結束),在該方法中,咱們調用了setState,出現了一系列奇怪的現象。所以,咱們再來看看setState方法,這裏只貼部分代碼。函數

1 ReactComponent.prototype.setState = function (partialState, callback) {
2   !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0;
3   this.updater.enqueueSetState(this, partialState);
4   if (callback) {
5     this.updater.enqueueCallback(this, callback, 'setState');
6   }
7 };

 

setState在調用時作了兩件事,第一,調用enqueueSetState。該方法將咱們傳入的partialState添加到一個叫作_pendingStateQueue的隊列中去存起來,而後執行一個enqueueUpdate方法。第二,若是存在callback就調用enqueueCallback將其存入一個_pendingCallbacks隊列中存起來。而後咱們來看enqueueUpdate方法。學習

 1 function enqueueUpdate(component) {
 2   ensureInjected();
 3 
 4   // Various parts of our code (such as ReactCompositeComponent's
 5   // _renderValidatedComponent) assume that calls to render aren't nested;
 6   // verify that that's the case. (This is called by each top-level update
 7   // function, like setState, forceUpdate, etc.; creation and
 8   // destruction of top-level components is guarded in ReactMount.)
 9 
10   if (!batchingStrategy.isBatchingUpdates) {
11     batchingStrategy.batchedUpdates(enqueueUpdate, component);
12     return;
13   }
14 
15   dirtyComponents.push(component);
16   if (component._updateBatchNumber == null) {
17     component._updateBatchNumber = updateBatchNumber + 1;
18   }
19 }

 

是否看到了某些熟悉的字眼,如isBatchingUpdates和batchedUpdates。不錯,其實翻閱代碼就能明白,這個batchingStrategy就是上面的ReactDefaultBatchingStrategy,只是它經過inject的形式對其進行賦值,比較隱蔽。所以,咱們當前的setState已經處於了這一類事務之中,isBatchingUpdates已經被置爲true,因此將會把它添加到dirtyComponents中,在某一時刻作批量更新。所以在前兩個setState中,並無作任何狀態更新,以及組件更新的事,而僅僅是將新的state和該組件存在了隊列之中,所以兩次都會打印出0,咱們以前的第一個問題就解決了,還有一個問題,咱們接着往下走。優化

在setTimeout中執行的setState打印出了2和3,有了前面的鋪墊,咱們大概就能得出結論,這應該就是由於這兩次setState分別執行了一次完整的事務,致使state被直接更新而形成的結果。那麼問題來了,爲何setTimeout中的setState會分別執行兩次不一樣的事務?以前執行ReactDOM.render開啓的事務在何時結束了?咱們來看下列代碼。this

 1 var RESET_BATCHED_UPDATES = {
 2   initialize: emptyFunction,
 3   close: function () {
 4     ReactDefaultBatchingStrategy.isBatchingUpdates = false;
 5   }
 6 };
 7 
 8 var FLUSH_BATCHED_UPDATES = {
 9   initialize: emptyFunction,
10   close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
11 };
12 
13 var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
14 
15 function ReactDefaultBatchingStrategyTransaction() {
16   this.reinitializeTransaction();
17 }
18 
19 _assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
20   getTransactionWrappers: function () {
21     return TRANSACTION_WRAPPERS;
22   }
23 });

 

這段代碼也是寫在ReactDefaultBatchingStrategy這個對象中的。咱們以前提到這個事務中transaction是ReactDefaultBatchingStrategyTransaction的實例,這段代碼其實就是給該事務添加了兩個在事務結束時會被調用的close方法。即在perform中的method執行完畢後,會按照這裏數組的順序[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]依次調用其close方法。FLUSH_BATCHED_UPDATES是執行批量更新操做。RESET_BATCHED_UPDATES咱們能夠看到將isBatchingUpdates變回false,即意味着事務結束。接下來再調用setState時,enqueueUpdate不會再將其添加到dirtyComponents中,而是執行batchingStrategy.batchedUpdates(enqueueUpdate, component)開啓一個新事務。可是須要注意,這裏傳入的參數是enqueueUpdate,即perform中執行的method爲enqueueUpdate,而再次調用該enqueueUpdate方法會去執行dirtyComponents那一步。這就能夠理解爲,處於單獨事務的setState也是經過將組件添加到dirtyComponents來完成更新的,只不過這裏是在enqueueUpdate執行完畢後當即執行相應的close方法完成更新,而前面兩個setState需在整個組件裝載完成以後,即在componentDidMount執行完畢後纔會去調用close完成更新。總結一下4個setState執行的過程就是:先執行兩次console.log,而後執行批量更新,再執行setState直接更新,執行console.log,最後再執行setState直接更新,再執行console.log,因此就會得出0,0,2,3。

案例二

以下兩種類似的寫法,得出不一樣的結果。

class Root extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    let me = this;
    me.setState({
      count: me.state.count + 1
    });
    me.setState({
      count: me.state.count + 1
    });
  }
  render() {
    return (
      <h1>{this.state.count}</h1>   //頁面中將打印出1
    )
  }
}
View Code
class Root extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    let me = this;
    me.setState(function(state, props) {
      return {
        count: state.count + 1
      }
    });
    me.setState(function(state, props) {
      return {
        count: state.count + 1
      }
    });
  }
  render() {
    return (
      <h1>{this.state.count}</h1>   //頁面中將打印出2
    )
  }
}
View Code

這兩種寫法,一個是在setState中傳入了object,一個是傳入了function,卻獲得了兩種不一樣的結果,這是什麼緣由形成的,這就須要咱們去深刻了解一下進行批量更行時都作了些什麼。

批量更新

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

1   performUpdateIfNecessary: function (transaction) {
2     if (this._pendingElement != null) {
3       ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
4     } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
5       this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
6     } else {
7       this._updateBatchNumber = null;
8     }
9   }
View Code

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

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

 1 _processPendingState: function (props, context) {
 2     var inst = this._instance;    // _instance保存了Constructor的實例,即經過ReactClass建立的組件的實例
 3     var queue = this._pendingStateQueue;
 4     var replace = this._pendingReplaceState;
 5     this._pendingReplaceState = false;
 6     this._pendingStateQueue = null;
 7 
 8     if (!queue) {
 9       return inst.state;
10     }
11 
12     if (replace && queue.length === 1) {
13       return queue[0];
14     }
15 
16     var nextState = _assign({}, replace ? queue[0] : inst.state);
17     for (var i = replace ? 1 : 0; i < queue.length; i++) {
18       var partial = queue[i];
19       _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
20     }
21 
22     return nextState;
23   },
View Code

什麼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的第二個參數,回調函數,它是在何時執行的。

案例三

 1 class Root extends React.Component {
 2     constructor(props) {
 3         super(props);
 4         this.state = {
 5             count: 0
 6         };
 7     }
 8 
 9     componentDidMount() {
10         let me = this;
11         setTimeout(function() {
12             me.setState({count: me.state.count + 1}, function() {
13                 console.log('did callback');
14             });
15             console.log('hello');
16         }, 0);
17     }
18 
19     componentDidUpdate() {
20         console.log('did update');
21     }
22 
23     render() {
24         return <h1>{this.state.count}</h1>
25     }
26 }
View Code

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

 1 var flushBatchedUpdates = function () {
 2   // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
 3   // array and perform any updates enqueued by mount-ready handlers (i.e.,
 4   // componentDidUpdate) but we need to check here too in order to catch
 5   // updates enqueued by setState callbacks and asap calls.
 6   while (dirtyComponents.length || asapEnqueued) {
 7     if (dirtyComponents.length) {
 8       var transaction = ReactUpdatesFlushTransaction.getPooled();
 9       transaction.perform(runBatchedUpdates, null, transaction);    // 處理批量更新
10       ReactUpdatesFlushTransaction.release(transaction);
11     }
12 
13     if (asapEnqueued) {
14       asapEnqueued = false;
15       var queue = asapCallbackQueue;
16       asapCallbackQueue = CallbackQueue.getPooled();
17       queue.notifyAll();    // 處理callback
18       CallbackQueue.release(queue);
19     }
20   }
21 };
View Code

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

總結一下

React在組件更新方面作了不少優化,這其中就包括了上述的批量更新。在componentDidMount中執行了N個setState,若是執行N次更新是件很傻的事情。React利用其獨特的事務實現,作了這些優化。正是由於這些優化,才形成了上面見到的怪現象。還有一點,再使用this.state時必定要注意組件的生命週期,不少時候在獲取state的時候,組件更新還未完成,this.state還未改變,這是很容易形成bug的一個地方,要避免這個問題,須要對組件生命週期有必定的瞭解。在執行setState時,咱們能夠經過在第一個參數傳入function的形式來避免相似的問題。若是你們發現有任何問題,均可以在評論中告訴我,感激涕零。

相關文章
相關標籤/搜索