摘要:區別在于傳入一個更新函數,就可以訪問當前狀態值。后面兩次會同步更新,分別輸出,很顯然,我們可以將次簡單規成兩類是一類中的又是一類,因為這兩次在不同的調用棧中執行。
寫業務代碼的時候 需要經常用到setState, 前幾天review代碼的時候, 又想了一下這個API, 發現對它的了解不是很清楚, 僅僅是 setState 是異步的, 周六在家參考了一些資料,簡單整理了下,寫的比較簡單, 通篇閱讀大概耗時 5min, 在這簡單分享一下, 希望對大家有所幫助 ;)。
先看一個例子假如有這樣一個點擊執行累加場景:
// … this.state = { count: 0, } incrementCount() { this.setState({ count: this.state.count + 1, }); } handleIncrement = () => { this.incrementCount(); this.incrementCount(); this.incrementCount(); } // ..
每一次點擊, 累加三次,看一下輸入:
并沒有達到預期的效果,糾正也很簡單:
incrementCount() { this.setState((prevState) => { return {count: prevState.count + 1} }); }
再看輸出:
setState 的時候, 一個傳入了object, 一個傳入了更新函數。
區別在于: 傳入一個更新函數,就可以訪問當前狀態值。 setState調用是 批量處理的,因此可以讓更新建立在彼此之上,避免沖突。
那問題來了, 為什么前一種方式就不行呢? 帶著這個疑問,繼續往下看。
setState為什么不會同步更新組件?
進入這個問題之前,我們先回顧一下現在對setState的認知:
1.setState不會立刻改變React組件中state的值.
2.setState通過觸發一次組件的更新來引發重繪.
3.多次setState函數調用產生的效果會合并。
重繪指的就是引起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 || setTimeout/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來刷新組件的view
執行之前阻塞的callback。
下面來看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是一類
setTimeOut中的又是一類,因為這兩次在不同的調用棧中執行。
我們先看看在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();
我只能說, 大胸弟, 你很騷。吾有舊友叼似汝,而今墳草丈許高。
結語
最后簡單重復下結論吧:
不要直接去操作this.state, 這樣會造成不必要的性能問題和隱患。
由React引發的事件處理,調用setState不會同步更新this.state,除此之外的setState調用會同步執行this.state。
我對這一套理論也不是特別熟悉, 如有紕漏, 歡迎指正 :)
擴展閱讀
https://reactjs.org/docs/faq-...
https://reactjs.org/docs/reac...
https://zhuanlan.zhihu.com/p/...
https://zhuanlan.zhihu.com/p/...
https://medium.com/@wisecobbl...
https://zhuanlan.zhihu.com/p/...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95904.html
摘要:承接上文,深入知識點整理一使用也滿一年了,從剛剛會使用到逐漸探究其底層實現,以便學習幾招奇技淫巧從而在自己的代碼中使用,寫出高效的代碼。有限狀態機,表示有限個狀態以及在這些狀態之間的轉移和動作等行為的模型。 承接上文,深入React知識點整理(一)使用React也滿一年了,從剛剛會使用到逐漸探究其底層實現,以便學習幾招奇技淫巧從而在自己的代碼中使用,寫出高效的代碼。下面整理一些知識點,...
摘要:前言樓主最近在整理的一些資料,為項目重構作準備,下午整理成了這篇文章。給傳入的是一個初始值,比如,這個按鈕的最初要顯示的是。取代了提供了一個統一的。 showImg(https://segmentfault.com/img/bVbpUle?w=900&h=550); Hooks are a new addition in React 16.8. They let you use sta...
摘要:前言樓主最近在整理的一些資料,為項目重構作準備,下午整理成了這篇文章。給傳入的是一個初始值,比如,這個按鈕的最初要顯示的是。取代了提供了一個統一的。 showImg(https://segmentfault.com/img/bVbpUle?w=900&h=550); Hooks are a new addition in React 16.8. They let you use sta...
摘要:整理一下中關于和的知識點。在任何應用中,數據都是必不可少的。比如,這樣關于,可以聲明為以下幾種類型注意,和是簡寫。是什么呢一個組件的顯示形態可以由數據狀態和外部參數所決定,外部參數也就是,而數據狀態就是。 整理一下React中關于state和props的知識點。 在任何應用中,數據都是必不可少的。我們需要直接的改變頁面上一塊的區域來使得視圖的刷新,或者間接地改變其他地方的數據。Rea...
摘要:殲轟運殲擊機轟炸機運輸機這里有個知識點需要知道這個一定要調用,這里相當于調用了的。下面簡單實現一下殲轟運殲擊機轟炸機運輸機如果要修改只能使用如改變。殲擊機轟炸機運輸機殲擊機轟炸機運輸機這樣一來,在頁面渲染前就初始化有值了,頁面也正常了。 每一個的地方,每一種的知識,每一種事物,都是從陌生到熟悉。在這個過程里面,或許能開闊眼界,增長見識,體驗樂趣。一切都歸于我們的心態與行動。 1.前言 ...
閱讀 2889·2021-09-22 15:20
閱讀 2961·2021-09-22 15:19
閱讀 3460·2021-09-22 15:15
閱讀 2389·2021-09-08 09:35
閱讀 2377·2019-08-30 15:44
閱讀 3009·2019-08-30 10:50
閱讀 3731·2019-08-29 16:25
閱讀 1591·2019-08-26 13:55