React原始碼解讀之setState

ZHONGJIAFENG7發表於2018-07-20

法國隊時隔20年奪得了世界盃冠軍,這個令人興奮的訊息到現在還在我內心不能平息,矯情過後開始著手分析一波,當然了分析的比較拙劣,希望能給學習react的人送去一點啟發和希望,寫的有所紕漏,不足之處還希望知識儲備是我幾倍的大牛們們指出。

正如大家一致公認的react是以資料為核心的,因此說到元件更新的時候我下意識的會想到當狀態變化的時候會進行元件更新。除了通過redux進行狀態管理從而進行元件更新外還有像我這種菜雞通過 setState進行狀態修改,關於setState,相信有不少人一開始和我有一樣的疑惑,為何不能通過普通的this.state來進行狀態更新,在開始前首先思考個問題,下面的列印結果分別是多少

 <!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的芳容。

setState

setState是元件原型鏈上的方法,引數為partialState, callback,看樣子長得還是比較55開的,引數也不多。提前預告幾個引數,_pendingStateQueue, dirtyComponents, isBatchingUpdates, internalInstance, transaction

    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:

// 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中

React原始碼解讀之setState

事務

這個時候問題來了,為何在當存入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操作

React原始碼解讀之setState

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長度為0

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不會再去更新元件

// 執行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的過程還是很優雅的,避免了重複無謂的重新整理元件。它的主要流程如下:

React原始碼解讀之setState
enqueueSetState將state放入佇列中,並呼叫enqueueUpdate處理要更新的Component 如果元件當前正處於update事務中,則先將Component存入dirtyComponent中,否則呼叫batchedUpdates處理,採用事務形式進行批量更新state。

最後結語抒發下程式設計師的小情懷,生活中總有那麼幾種方式能夠讓你快速提高,比如身邊有大牛,或者通過學習原始碼都是不錯的選擇,我很幸運這兩個條件目前都能夠滿足到,廢話不多說了,時間很晚了洗洗睡了,學習react還依舊長路漫漫,未來得加倍努力才是。

參考

深入react技術棧

CSDN

簡書

掘金

知乎

gitbook

相關文章