React源碼解讀之setState

法國隊時隔20年奪得了世界盃冠軍,這個使人興奮的消息到如今還在我心裏不能平息,矯情事後開始着手分析一波,固然了分析的比較拙劣,但願能給學習react的人送去一點啓發和但願,寫的有所紕漏,不足之處還但願知識儲備是我幾倍的大牛們們指出。javascript

正如你們一致公認的react是以數據爲核心的,所以說到組件更新的時候我下意識的會想到當狀態變化的時候會進行組件更新。除了經過redux進行狀態管理從而進行組件更新外還有像我這種菜雞經過 setState進行狀態修改,關於setState,相信有很多人一開始和我有同樣的疑惑,爲什麼不能經過普通的this.state來進行狀態更新,在開始前首先思考個問題,下面的打印結果分別是多少html

<!DOCTYPE html>
    <html lang="en">
    	<head>
    	    <meta charset="UTF-8">
    		<title>Document</title>
    		<script src="./js/react.js"></script>
    		<script src="./js/react-dom.js"></script>
    		<script src="./js/browser.js"></script>
    		<script type="text/babel">
    		
        		class Comp extends React.Component{
    				constructor(...args) {
    					super(...args);
    					this.state = {i: 0}
    			    }

    		        render(){
    			        return <div onClick={() => {
    					    this.setState({i: this.state.i + 1})
    					    console.log('1',this.state.i);     // 1
    
    					    this.setState({i: this.state.i + 1})
    					    console.log('2',this.state.i);     // 2
    
    					    setTimeout(() => {
    					        this.setState({i: this.state.i + 1})
    					        console.log('3',this.state.i)  // 3
    
    					        this.setState({i: this.state.i + 1})
    					        console.log('4',this.state.i)  // 4
    					    },0);
    
    					    this.setState((prevState, props) => ({
    					        i: prevState.i + 1
    					    }));
    				    }}>Hello, world!{this.state.i} <i>{this.props.name}, 年齡{this.props.age}</i></div>;
    				}
    			}
    			window.onload = function(){
    				var oDiv = document.getElementById('div1');
    				ReactDOM.render(
    					<Comp name="zjf" age='24'/>,
    					oDiv
    				);
    			}
    		</script>
    	</head>
    	<body>
    		<div id="div1"></div>
    	</body>
    </html>
複製代碼

這個雖然寥寥幾個參數卻對我來講像解決奧數題那樣複雜,1234? 0012? 。。心裏的無限熱情也就在這個時候隨着夏季的溫度上升再上升了,默默打開了源代碼,其中的實現真的能夠說amazing,帶着這個疑問讓咱們首先來看一下setState的芳容。java

setState

setState是組件原型鏈上的方法,參數爲partialState, callback,看樣子長得仍是比較55開的,參數也很少。提早預告幾個參數,_pendingStateQueue, dirtyComponents, isBatchingUpdates, internalInstance, transactionreact

ReactComponent.prototype.setState = function (partialState, callback) {
      !(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;
      this.updater.enqueueSetState(this, partialState);
      // 若是有回調函數,在狀態進行更新後執行
      if (callback) {
        this.updater.enqueueCallback(this, callback, 'setState');
      }
    };   
複製代碼

enqueueSetState:git

// updater是存放更新操做方法的一個類
updater.enqueueSetState
// mountComponent時把ReactElement做爲key,將ReactComponent存入了map中
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// _pendingStateQueue:待更新隊列,若是_pendingStateQueue的值爲null,將其賦值爲空數組[],並將partialState放入待更新state隊列_pendingStateQueue,最後執行enqueueUpdate(internalInstance)
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
複製代碼

enqueueUpdate:程序員

// getInitialState,componentWillMount,render,componentWillUpdate中setState都不會引發updateComponent
// 經過isBatchingUpdates來判斷是否處於批量更新的狀態
batchingStrategy.isBatchingUpdates!==true ?
batchingStrategy.batchedUpdates(enqueueUpdate, component);
:dirtyComponents.push(component);
複製代碼

須要注意的是點擊事件的處理自己就是在一個大的事務中,isBatchingUpdates已是true了,因此前兩次的setState以及最後一次過程的component,都被存入了dirtyComponent中redux

事務

這個時候問題來了,爲什麼在當存入dirtyComponent中的時候,什麼時候進行更新操做,要知道這個須要至少batchingStrategy的構成以及事務的原理,首先injectBatchingStrategy是經過injectBatchingStrategy進行注入,參數爲ReactDefaultBatchingStrategy,具體代碼以下:
//batchingStrategy批量更新策略
ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
      // 簡單來講事務有initiation->執行callback->close過程,callback即爲enqueueUpdate方法
      // transaction經過new ReactDefaultBatchingStrategyTransaction()生成,最後經過一系列調用return TRANSACTION_WRAPPERS
      return transaction.perform(callback, null, a, b, c, d, e);
  }
}
複製代碼
// 設置兩個wrapper,RESET_BATCHED_UPDATES設置isBatchingUpdates,FLUSH_BATCHED_UPDATES會在一個transaction的close階段運行runBatchedUpdates,從而執行update
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    // 事務批更新處理結束時,將isBatchingUpdates設爲了false
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  // 關鍵步驟
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
複製代碼

下面圖片很好地解釋了事務的流程,配合TRANSACTION_WRAPPERS的兩個對象,perform會依次調用這兩個對象內的initialize方法進行初始化操做,而後執行method,最後依次調用這兩個對象內的close進行isBatchingUpdates重置以及狀態的更新,因爲JS的單線程機制,因此每條事務都會依次執行。所以也就有了isBatchingUpdates從false->true->false的過程,這也就意味着partialState不會被存入dirtyComponent中,而是調用batchingStrategy.batchedUpdates(enqueueUpdate, component),進行initialize->enqueueUpdate->close更新state操做數組

perform: function (method, scope, a, b, c, d, e, f) {
    var errorThrown;
    var ret;
    try {
      this._isInTransaction = true;
      errorThrown = true;
      // 先運行全部transactionWrappers中的initialize方法,開始索引爲0
      this.initializeAll(0);
      // 再執行perform方法傳入的callback,也就是enqueueUpdate
      ret = method.call(scope, a, b, c, d, e, f);
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
          // 最後運行wrapper中的close方法,endIndex爲0
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          // 最後運行wrapper中的close方法
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  }
複製代碼
initializeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    // 遍歷全部註冊的wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
      // 調用wrapper的initialize方法
      this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
    }
  }
複製代碼
closeAll: function (startIndex) {
	var transactionWrappers = this.transactionWrappers;
	// 遍歷全部wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) 
    {
		var wrapper = transactionWrappers[i];
	    var initData = this.wrapperInitData[i];
	    errorThrown = true;
	    if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
          // 調用wrapper的close方法
          wrapper.close.call(this, initData);
        }
        ....
     }
  }
複製代碼
var flushBatchedUpdates = function () {
  // 循環遍歷處理完全部dirtyComponents,若是dirtyComponents長度大於1也只執行1次,由於在更新操做的時候會將dirtyComponents設置爲null
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      // close前執行完runBatchedUpdates方法
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};
複製代碼
runBatchedUpdates(){
    // 執行updateComponent
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

}
複製代碼

close前執行完runBatchedUpdates方法, 確定有人和我有同樣的疑惑這樣在事務結束的時候調用事務內的close再次進行調用flushBatchedUpdates,不是循環調用一直輪迴了嗎,全局搜索下TRANSACTION_WRAPPERS,發現更新完成之後是一個全新的兩個對象,兩個close方法,包括設置dirtyComponent長度爲0,設置context,callbacks長度爲0babel

var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];

var NESTED_UPDATES = {
  initialize: function () {
    this.dirtyComponentsLength = dirtyComponents.length;
  },
  close: function () {
    if (this.dirtyComponentsLength !== dirtyComponents.length) {
      dirtyComponents.splice(0, this.dirtyComponentsLength);
      // 關鍵步驟
      flushBatchedUpdates();
    } else {
      dirtyComponents.length = 0;
    }
  }
};

var UPDATE_QUEUEING = {
  initialize: function () {
    this.callbackQueue.reset();
  },
  close: function () {
    this.callbackQueue.notifyAll();
  }
};
複製代碼

執行updateComponent進行狀態更新,值得注意的是更新操做內會調用_processPendingState進行原有state的合併以及設置this._pendingStateQueue = null,這也就意味着dirtyComponents進入下一次循環時,執行performUpdateIfNecessary不會再去更新組件app

// 執行updateComponent,從而刷新View 
performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
    }
    // 在setState更新中,其實只會用到第二個 this._pendingStateQueue !== null 的判斷,即若是_pendingStateQueue中還存在未處理的state,那就會執行updateComponent完成更新。
    else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      // 執行updateComponent,從而刷新View 
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
    } else {
      this._updateBatchNumber = null;
    }
}
複製代碼
_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];
      // 進行合併操做,若是爲partial類型function執行後進行合併
      _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
    }
    
    return nextState;
},
複製代碼

實現setState同步更新

經過上述能夠知道,直接修改state,並不會從新觸發render,state的更新是一個合併的過程,當使用異步或者callback的方式會使得更新操做以事務的形式進行。所以能夠比較容易地解答以前的那個疑問,答案爲0,0,3,4。固然若是想實現setState同步更新,大概能夠用着三個方法:

// 實現同步辦法
// 方法一:
incrementCount(){
	this.setState((prevState, props) => ({
	  count: prevState.count + 1
	}));
	this.setState((prevState, props) => ({
	  count: prevState.count + 1
	}));
}
// 方法二:
incrementCount(){
    setTimeout(() => {
	    this.setState({
		  count: prevState.count + 1
		});
		this.setState({
		  count: prevState.count + 1
		});
    }, 0)
}
// 方法三:
incrementCount(){
    this.setState({
	  count: prevState.count + 1
	},() => {
		this.setState({
		  count: prevState.count + 1
		});
	});
}
複製代碼

總結

總的來講setState的過程仍是很優雅的,避免了重複無謂的刷新組件。它的主要流程以下:

enqueueSetState將state放入隊列中,並調用enqueueUpdate處理要更新的Component 若是組件當前正處於update事務中,則先將Component存入dirtyComponent中,不然調用batchedUpdates處理,採用事務形式進行批量更新state。

最後結語抒發下程序員的小情懷,生活中總有那麼幾種方式可以讓你快速提升,好比身邊有大牛,或者經過學習源代碼都是不錯的選擇,我很幸運這兩個條件目前都可以知足到,廢話很少說了,時間很晚了洗洗睡了,學習react還依舊長路漫漫,將來得加倍努力纔是。

參考

深刻react技術棧

CSDN

簡書

掘金

知乎

gitbook

相關文章
相關標籤/搜索