寫業務代碼的時候 須要常常用到setState, 前幾天review代碼的時候, 又想了一下這個API, 發現對它的瞭解不是很清楚, 僅僅是 setState
是異步的, 週六在家參考了一些資料,簡單整理了下,寫的比較簡單, 通篇閱讀大概耗時 5min, 在這簡單分享一下, 但願對你們有所幫助 ;)。html
假若有這樣一個點擊執行累加場景:前端
this.state = { count: 0, } incrementCount() { this.setState({ count: this.state.count + 1, }); } handleIncrement = () => { this.incrementCount(); this.incrementCount(); this.incrementCount(); }
每一次點擊, 累加三次,看一下輸入:react
並無達到預期的效果,糾正也很簡單:數組
incrementCount() { this.setState((prevState) => { return {count: prevState.count + 1} }); }
再看輸出:app
setState 的時候, 一個傳入了object, 一個傳入了更新函數。異步
區別在於: 傳入一個更新函數
,就能夠訪問當前狀態值。 setState調用是 批量處理
的,所以可讓更新創建在彼此之上,避免衝突。函數
那問題來了, 爲何前一種方式就不行呢? 帶着這個疑問,繼續往下看。性能
setState 爲何不會同步更新組件
?this
進入這個問題以前,咱們先回顧一下如今對 setState 的認知:spa
不會馬上改變
React組件中state的值.觸發一次組件的更新
來引起重繪
.合併
。重繪指的就是引發 React 的更新生命週期函數4個函數:
shouldComponentUpdate
(被調用時this.state沒有更新;若是返回了false,生命週期被中斷,雖然不調用以後的函數了,可是state仍然會被更新)componentWillUpdate
(被調用時this.state沒有更新)render
(被調用時this.state獲得更新)componentDidUpdate
若是每一次 setState 調用都走一圈生命週期,光是想想也會以爲會帶來性能的問題,其實這四個函數都是純函數,性能應該還好,可是render函數返回的結果會拿去作Virtual DOM比較和更新DOM樹,這個就比較費時間。
目前React會將setState的效果放在隊列
中,積攢着一次引起更新
過程。
爲的就是把 Virtual DOM 和 DOM 樹操做降到最小,用於提升性能
。
查閱一些資料後發現,某些操做仍是能夠同步更新 this.state的。
setState 何時會執行同步更新
?
先直接說結論
吧:
在React中,若是是由React引起的事件處理(好比經過onClick引起的事件處理),調用 setState 不會同步更新 this.state,除此以外的setState調用會同步執行this.state。
所謂「除此以外」,指的是繞過React經過addEventListener
直接添加的事件處理函數,還有經過setTimeout
|| setInterval
產生的異步調用。
簡單一點說, 就是通過React 處理的事件是不會同步更新
this.state的. 經過 addEventListener
|| setTimeou
t/setInterval
的方式處理的則會同步更新
。
具體能夠參考 jsBin 的這個例子。
結果就很清晰了:
點擊Increment ,執行onClick ,輸出0;
而經過addEventListener , 和 setTimeout 方式處理的, 第一次 直接輸出了1;
理論大概是這樣的,盜用一張圖:
在React的setState函數實現中,會根據一個變量 isBatchingUpdates
判斷是直接更新
this.state 仍是放到隊列
中。
而isBatchingUpdates
默認是false
,也就表示setState會同步更新
this.state,可是有一個函數batchedUpdates
。
這個函數會把isBatchingUpdates
修改成true
,而當React在調用事件處理函數以前就會調用這個batchedUpdates
,形成的後果,就是由React控制的事件處理過程setState不會同步更新
this.state。
經過上圖,咱們知道了大體流程, 要想完全瞭解它的機制,咱們解讀一下源碼。
探祕setState 源碼
// setState方法入口以下: ReactComponent.prototype.setState = function (partialState, callback) { // 將setState事務放入隊列中 this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); }};
相關的幾個概念:
partialState
,有部分state的含義,可見只是影響涉及到的state,不會傷及無辜。enqueueSetState
是 state 隊列管理的入口方法,比較重要,咱們以後再接着分析。replaceState
:
replaceState: function (newState, callback) { this.updater.enqueueReplaceState(this, newState); if (callback) { this.updater.enqueueCallback(this, callback, 'replaceState'); }}, replaceState中取名爲newState,有徹底替換的含義。一樣也是以隊列的形式來管理的。
enqueueSetState
enqueueSetState: function (publicInstance, partialState) { // 先獲取ReactComponent組件對象 var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState'); if (!internalInstance) { return; } // 若是_pendingStateQueue爲空,則建立它。能夠發現隊列是數組形式實現的 var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); // 將要更新的ReactComponent放入數組中 enqueueUpdate(internalInstance);}
其中getInternalInstanceReadyForUpdate
源碼以下
function getInternalInstanceReadyForUpdate(publicInstance, callerName) { // 從map取出ReactComponent組件,還記得mountComponent時把ReactElement做爲key,將ReactComponent存入了map中了吧,ReactComponent是React組件的核心,包含各類狀態,數據和操做方法。而ReactElement則僅僅是一個數據類。 var internalInstance = ReactInstanceMap.get(publicInstance); if (!internalInstance) { return null; } return internalInstance;}
enqueueUpdate源碼以下:
function enqueueUpdate(component) { ensureInjected(); // 若是不是正處於建立或更新組件階段,則處理update事務 if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 若是正在建立或更新組件,則暫且先不處理update,只是將組件放在dirtyComponents數組中 dirtyComponents.push(component);}
batchedUpdates
batchedUpdates: function (callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; // 批處理最開始時,將isBatchingUpdates設爲true,代表正在更新 ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations if (alreadyBatchingUpdates) { callback(a, b, c, d, e); } else { // 以事務的方式處理updates,後面詳細分析transaction transaction.perform(callback, null, a, b, c, d, e); }} var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { // 事務批更新處理結束時,將isBatchingUpdates設爲了false ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
enqueueUpdate
包含了React避免重複render
的邏輯。
mountComponent
和 updateComponent
方法在執行的最開始,會調用到 batchedUpdates
進行批處理
更新,此時會將isBatchingUpdates
設置爲true
,也就是將狀態標記爲如今正處於更新階段
了。
以後React以事務的方式處理組件update,事務處理完後會調用wrapper.close() 。
而TRANSACTION_WRAPPERS
中包含了RESET_BATCHED_UPDATES
這個wrapper
,故最終會調用RESET_BATCHED_UPDATES.close()
, 它最終會將isBatchingUpdates
設置爲false
。
故 getInitialState
,componentWillMount
, render
,componentWillUpdate
中 setState
都不會引發 updateComponent
。
但在componentDidMount
和 componentDidUpdate
中則會。
事務經過wrapper
進行封裝。
一個wrapper
包含一對 initialize
和 close
方法。好比 RESET_BATCHED_UPDATES
:
var RESET_BATCHED_UPDATES = { // 初始化調用 initialize: emptyFunction, // 事務執行完成,close時調用 close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};
transcation被包裝在wrapper中,好比:
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
transaction
是經過transaction.perform(callback, args…)
方法進入的,它會先調用註冊好的wrapper
中的initialize
方法,而後執行perform
方法中的callback
,最後再執行close
方法。
下面分析transaction.perform(callback, args…)
perform: function (method, scope, a, b, c, d, e, f) { var errorThrown; var ret; try { this._isInTransaction = true; errorThrown = true; // 先運行全部wrapper中的initialize方法 this.initializeAll(0); // 再執行perform方法傳入的callback ret = method.call(scope, a, b, c, d, e, f); errorThrown = false; } finally { try { if (errorThrown) { // 最後運行wrapper中的close方法 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]; try { this.wrapperInitData[i] = Transaction.OBSERVED_ERROR; // 調用wrapper的initialize方法 this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null; } finally { if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) { try { this.initializeAll(i + 1); } catch (err) {} } } } }, 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]; var errorThrown; try { errorThrown = true; if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) { // 調用wrapper的close方法,若是有的話 wrapper.close.call(this, initData); } errorThrown = false; } finally { if (errorThrown) { try { this.closeAll(i + 1); } catch (e) {} } } } this.wrapperInitData.length = 0; }
更新組件: runBatchedUpdates
前面分析到enqueueUpdate
中調用transaction.perform(callback, args...)
後,發現,callback
仍是enqueueUpdate
方法啊,那豈不是死循環
了?不是說好的setState
會調用updateComponent
,從而自動刷新View的嗎? 咱們仍是要先從transaction事務提及。
咱們的wrapper中註冊了兩個wrapper,以下:
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
RESET_BATCHED_UPDATES
用來管理isBatchingUpdates
狀態,咱們前面在分析setState是否當即生效時已經講解過了。
那FLUSH_BATCHED_UPDATES
用來幹嗎呢?
var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)}; var flushBatchedUpdates = function () { // 循環遍歷處理完全部dirtyComponents 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); } }};
FLUSH_BATCHED_UPDATES
會在一個transaction
的close
階段運行runBatchedUpdates
,從而執行update
。
function runBatchedUpdates(transaction) { var len = transaction.dirtyComponentsLength; dirtyComponents.sort(mountOrderComparator); for (var i = 0; i < len; i++) { // dirtyComponents中取出一個component var component = dirtyComponents[i]; // 取出dirtyComponent中的未執行的callback,下面就準備執行它了 var callbacks = component._pendingCallbacks; component._pendingCallbacks = null; var markerName; if (ReactFeatureFlags.logTopLevelRenders) { var namedComponent = component; if (component._currentElement.props === component._renderedComponent._currentElement) { namedComponent = component._renderedComponent; } } // 執行updateComponent ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction); // 執行dirtyComponent中以前未執行的callback if (callbacks) { for (var j = 0; j < callbacks.length; j++) { transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance()); } } }}
runBatchedUpdates
循環遍歷dirtyComponents
數組,主要幹兩件事。
下面來看performUpdateIfNecessary
:
performUpdateIfNecessary: function (transaction) { if (this._pendingElement != null) { // receiveComponent會最終調用到updateComponent,從而刷新View ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context); } if (this._pendingStateQueue !== null || this._pendingForceUpdate) { // 執行updateComponent,從而刷新View。這個流程在React生命週期中講解過 this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); } },
最後驚喜的看到了receiveComponent
和updateComponent
吧。
receiveComponent
最後會調用updateComponent
,而updateComponent
中會執行React組件存在期的生命週期方法,
如componentWillReceiveProps
, shouldComponentUpdate
, componentWillUpdate
,render
, componentDidUpdate
。
從而完成組件更新的整套流程。
總體流程回顧:
1.enqueueSetState將state放入隊列中,並調用enqueueUpdate處理要更新的Component
2.若是組件當前正處於update事務中,則先將Component存入dirtyComponent中。不然調用batchedUpdates處理。
3.batchedUpdates發起一次transaction.perform()事務
4.開始執行事務初始化,運行,結束三個階段
5.初始化:事務初始化階段沒有註冊方法,故無方法要執行
6.運行:執行setSate時傳入的callback方法,通常不會傳callback參數
7.結束:更新isBatchingUpdates爲false,並執行FLUSH_BATCHED_UPDATES這個wrapper中的close方法
8.FLUSH_BATCHED_UPDATES在close階段,會循環遍歷全部的dirtyComponents,調用updateComponent刷新組件,並執行它的pendingCallbacks, 也就是setState中設置的callback。
看完理論, 咱們再用一個例子鞏固下.
class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log('第 1 次 log:', this.state.val); this.setState({val: this.state.val + 1}); console.log('第 2 次 log:', this.state.val); setTimeout(() => { this.setState({val: this.state.val + 1}); console.log('第 3 次 log:', this.state.val); this.setState({val: this.state.val + 1}); console.log('第 4 次 log:', this.state.val); }, 0); } render() { return null; } };
前兩次在isBatchingUpdates
中,沒有更新state, 輸出兩個0。
後面兩次會同步更新, 分別輸出2, 3;
很顯然,咱們能夠將4次setState簡單規成兩類
:
咱們先看看在componentDidMount中setState的調用棧:
再看看在setTimeOut中的調用棧:
咱們重點看看在componentDidMount中的sw3e調用棧 :
發現了batchedUpdates
方法。
原來在setState調用以前,就已經處於batchedUpdates執行的事務之中
了。
那batchedUpdates
方法是誰調用的呢?咱們再往上追溯一層,原來是ReactMount.js中的_renderNewRootComponent方法。
也就是說,整個將React組件渲染到DOM的過程就處於一個大的事務中了。
接下來就很容易理解了: 由於在componentDidMount
中調用setState
時,batchingStrategy
的isBatchingUpdates
已經被設置爲true
,因此兩次setState的結果並無當即生效,而是被放進了dirtyComponents
中。
這也解釋了兩次打印this.state.val都是0
的緣由,由於新的state還沒被應用到組件中。
再看setTimeOut中的兩次setState,由於沒有前置的batchedUpdate
調用,因此batchingStrategy
的isBatchingUpdates
標誌位是false
,也就致使了新的state
立刻生效,沒有走到dirtyComponents
分支。
也就是說,setTimeOut中的第一次執行,setState時,this.state.val爲1;
而setState完成後打印時this.state.val變成了2。
第二次的setState同理。
經過上面的例子,咱們就知道setState 是能夠同步更新
的,可是仍是儘可能避免直接使用, 僅做了解就能夠了。
若是你非要玩一些騷操做,寫出這樣的代碼去直接去操做this.state:
this.state.count = this.state.count + 1; this.state.count = this.state.count + 1; this.state.count = this.state.count + 1; this.setState();
我只能說, 大胸弟, 你很騷。吾有舊友叼似汝,而今墳草丈許高。
最後簡單重複下結論吧:
我對這一套理論也不是特別熟悉, 若有紕漏, 歡迎指正 :)
下面是個人公衆號: 前端e進階
有新內容會第一時間更新在這裏, 但願你們多多關注, 觀看最新內容。
擴展閱讀
https://reactjs.org/docs/faq-...
https://reactjs.org/docs/reac...
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...