對React setState的一些思考與心得
前言
這篇文章主要是為了紀錄一些自己對於setState的認識的不斷深入的過程。我覺得這過程對我自己來說很有價值,不光是層層遞進的瞭解一個API的執行機制,更對react總體的設計有了更為深入的認識。
第一階段 初識setState
使用過React的應該都知道,在React中,一個元件中要讀取當前狀態需要訪問this.state,但是更新狀態卻需要使用this.setState,不是直接在this.state上修改,就比如這樣:
//讀取狀態const count = this.state.count;//更新狀態this.setState({count: count + 1});//無意義的修改this.state.count = count + 1;
其實這主要有幾點考慮,首先this.state說到底只是一個物件,單純的去修改一個物件的值是毫無意義的,在React中只有去驅動UI的更新才會有意義,因此雖然我們可以嘗試直接改變this.state,但並沒有驅動UI的重新渲染,因此這種操作也就毫無意義。也正是由於這個原因,我們就需要使用this.setState來驅動元件的更新過程。
然後在我剛學習React時,我就看見了這段很經典的程式碼:
function incrementMultiple() { this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1}); }
作為一名JSer,我看完就毫不猶豫的想到,這特麼不就是count的值加3麼,但轉眼看了下面的答案,光速打臉,實際的結果是state只增加了1。然後我就不由想到當時沒怎麼看懂的React文件中的一些話:狀態更新可能是非同步的,狀態更新合併。恩,沒毛病,因為非同步且會合並,因此這三條語句合併為一條語句了,所以就只執行一次。然後就扭頭溜了,並沒有去思考一些深層次的問題。
第二階段 setState理解的進階
但是隨著對React的理解的逐步加深,我開始對setState有了更加深的理解:
首先我意識到this.setState會透過引發一次元件的更新過程來引發重新繪製。也就是說setState的呼叫會引起React的更新生命週期的四個函式的依次呼叫:
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
我們都知道,在React生命週期函式里,以render函式為界,無論是掛載過程和更新過程,在render之前的幾個生命週期函式,this.state和Props都是不會發生更新的,直到render函式執行完畢後,this.state才會得到更新。(有一個例外:當shouldComponentUpdate函式返回false,這時候更新過程就被中斷了,render函式也不會被呼叫了,這時候React不會放棄掉對this.state的更新的,所以雖然不呼叫render,依然會更新this.state。)
React的官方文件有提到過這麼一句話:
狀態更新會合並(也就是說多次setstate函式呼叫產生的效果會合並)。
起初我對這句話理解並不是很深刻,但按照官方文件的程式碼示例寫了這麼一段程式碼:
function updateName() { this.setState({Age: '22'}) this.setState({Name: 'srtian'}) }
果然執行結果與以下程式碼是等價的
function updateName() { this.setState({Age: '22', Name: 'srtian}) }
於是我將其理解為一個佇列,每個this.setState()都會被合併起來,排成一排,到最後一次解決。但對其設計的原因並不理解,只知道這樣有利於效能(也是在文件上看到的)。
直到理解上面React生命週期函式的原理後,我才理解了setState關於這個設計的意圖。
前面我們提到過,每一次使用setState都會呼叫一次更新的生命週期,如果每一次this.serState()都呼叫一次上面那四個生命週期函式,雖然以上四個函式都是純函式,效能浪費上還好,但render函式會將結果拿去做Virtual DOM比較和更新DOM樹,這個就比較費時間。因此,將多個this.setSate進行合併,render函式就能夠將合併後的this.setState()的結果一次性的與Virtual DOM比較然後更新DOM樹,這樣就能夠用有效的提升效能。
除此之外,我還認為setState的設計十分巧妙,一般來說只在render函式後才會進行更新this.state。這其實也避免了React16的Fiber可能會產生的一個問題:由於Fiber下的元件更新是可以中斷,也就是說在一個元件的更新過程中,可能更新到一半的時候就由於其他原因而中斷更新,回去做更重要的事情了,在做完更重要的事情後,再回來更新這個元件,這會導致前面的那些生命週期函式可能會執行多次。因此如果在render之前this.setState()就改變狀態的話,很有可能就會導致元件狀態的多次更新,從而導致元件狀態的混亂。
第三階段 從原始碼理解setstate
這是React15.6版本,由於React16變動較大,setState的呼叫棧發生變動,因此僅供參考。
經歷了上面那個階段,我算是對setState有那麼一些理解了,但還是不能理解很多東西比如:this.setState()的是怎麼合併的?setState()到底是怎樣一種騷操作?...等等。然後我又看見了這段經典的程式碼:
class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 1 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 2 次 log setTimeout(() => { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 3 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 4 次 log }, 0); } render() { return null; } };
恩!按照我多年經驗,這波操作我看不懂!
image
於是硬著頭皮開啟了React原始碼,開始一波瞎分析:
首先就是setState了,可以看出它接受兩個引數partialState和callback,其中partialState顧名思義就是部分state,起這個名字也能就是想表達它的state沒有改變(瞎猜的。。。)。以下是省略了一部分的程式碼,只看核心部分。
ReactComponent.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } }; enqueueSetState: function(publicInstance, partialState) { if (__DEV__) { ReactInstrumentation.debugTool.onSetState(); warning( partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().', ); } var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState', ); if (!internalInstance) { return; } var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); }// 透過enqueueUpdate執行state更新function enqueueUpdate(component) { ensureInjected(); // batchingStrategy是批次更新策略,isBatchingUpdates表示是否處於批次更新過程 // 最開始預設值為false if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; } }// 對_pendingElement, _pendingStateQueue, _pendingForceUpdate進行判斷,// _pendingStateQueue由於會對state進行修改,所以不為空,// 然後會呼叫updateComponent方法performUpdateIfNecessary: function(transaction) { if (this._pendingElement != null) { ReactReconciler.receiveComponent( this, this._pendingElement, transaction, this._context, ); } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) { this.updateComponent( transaction, this._currentElement, this._currentElement, this._context, this._context, ); } else { this._updateBatchNumber = null; } },
其中這段程式碼需要額外注意:
// batchingStrategy是批次更新策略,isBatchingUpdates表示是否處於批次更新過程 // 最開始預設值為falseif (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component);if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; }
上面這段程式碼的意思就是如果是處於批次更新模式,也就是isBatchingUpdates為true時,不進行state的更新操作,而是將需要更新的component新增到dirtyComponents陣列中。
如果不處於批次更新模式,則對所有佇列中的更新執行batchedUpdates方法。
然後可以找到了這個batchedUpdates:
var ReactDefaultBatchingStrategy = { // 也就是上面提到的預設為false isBatchingUpdates: false, // 這個方法只有在isBatchingUpdates: false時才會呼叫 // 但一般來說,處於react大事務中時,會在render中的_renderNewRootComponent中將其設定為true。 batchedUpdates: function(callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations if (alreadyBatchingUpdates) { return callback(a, b, c, d, e); } else { return transaction.perform(callback, null, a, b, c, d, e); } },
看到這我總算理解了,當我們呼叫setState時,最終會透過enqueueUpdate執行state更新,就像上面那樣有兩種更新的模式,一種是批次更新模式,將組建儲存在dirtyComponents;另一種非批次模式,將會遍歷dirtyComponents,對每一個dirtyComponents呼叫updateComponent方法。就像這張圖:
流程圖
至於批次與非批次模式,會透過ReactDefaultBatchingStrategy中的isBatchingUpdates屬性來進行判斷。在非批次模式下,會立即應用新的state;而在批次模式下,需要更新state的元件會被push 到dirtyComponents,再執行更新。
所以我們再看前面的那坨程式碼:
class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 1 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 2 次 log setTimeout(() => { this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 3 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 4 次 log }, 0); } render() { return null; } };
就不難看出它的答案是 0, 0, 2, 3。
總結起來就是這樣:
this.setState首先會把state推入pendingState佇列中
然後將元件標記為dirty
React中有事務的概念,最常見的就是更新事務,如果不在事務中,則會開啟一次新的更新事務,更新事務執行的操作就是把元件標記為dirty。
判斷是否處於batch update
是的話,儲存組建於dirtyComponent中,在事務結束的時候才會透過 ReactUpdates.flushBatchedUpdates 方法將所有的臨時 state merge 並計算出最新的 props 及 state,然後將其批次執行,最後再關閉結束事務。
不是的話,直接開啟一次新的更新事務,在標記為dirty之後,直接開始更新元件。因此當setState執行完畢後,元件就更新完畢了,所以會造成定時器同步更新的情況。
另外還有就是updateComponent方法,這也很重要:
{ // 會檢測元件中的state和props是否發生變化,有變化才會進行更新; // 如果shouldUpdateComponent函式中返回false則不會執行元件的更新 updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext,) { var inst = this._instance; var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = true; if (!this._pendingForceUpdate) { if (inst.shouldComponentUpdate) { if (__DEV__) { shouldUpdate = measureLifeCyclePerf( () => inst.shouldComponentUpdate(nextProps, nextState, nextContext), this._debugID, 'shouldComponentUpdate', ); } else { shouldUpdate = inst.shouldComponentUpdate( nextProps, nextState, nextContext, ); } } else { if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); } } } },// 該方法會合並需要更新的state,然後加入到更新佇列中 _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 = Object.assign({}, replace ? queue[0] : inst.state); for (var i = replace ? 1 : 0; i發現它會呼叫shouldComponentUpdate和componentWillUpdate方法,看到這不由理解了一個定律:不要在shouldComponentUpdate和componentWillUpdate中呼叫setState。如果在這兩個生命週期裡呼叫setState,會造成造成迴圈呼叫。
作者:Srtian
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1806/viewspace-2810092/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 對react中setState的總結React
- 對於封裝react元件的一些思考封裝React元件
- 從一次react非同步setState引發的思考React非同步
- 揭密React setStateReact
- React之setStateReact
- 【React】setState詳解React
- React淺談setStateReact
- vue與react元件的思考VueReact元件
- React的setState執行機制React
- react 與 vue 使用心得ReactVue
- [譯]理解react之setStateReact
- React-setState雜記React
- 軟體開發的一些思考及心得體會
- 對ThreadLocal的一些思考thread
- 對於人生的一些思考
- 關於REACT正規化的一些思考React
- 對設計與設計師“價值”的一些思考
- 【React深入】setState的執行機制React
- react 常見setState的原理解析React
- React原始碼閱讀:setStateReact原始碼
- Understanding React `setState` 翻譯React
- React setState是非同步嗎?React非同步
- 對React一些原理的理解React
- 關於React中動畫不生效的一些思考React動畫
- 後端的一些經驗與心得後端
- React中setState真的是非同步的嗎React非同步
- React之setState的正確開啟方式React
- [React技術內幕] setState的祕密React
- 關於react中setState的深入理解React
- 對格式化字串的一些思考字串
- 對提高HBase寫效能的一些思考
- 對於最近的一些理解和思考
- 對自己目前狀況的一些思考
- React菜鳥入門之setStateReact
- React中setState修改深層物件React物件
- React原始碼解讀之setStateReact原始碼
- React 原始碼剖析系列 - 解密 setStateReact原始碼解密
- React元件的DidMount事件裡的setState事件React元件事件