React官網對於setState的說明:
將setState()認為是一次請求而不是一次立即執行更新元件的命令。為了更為可觀的效能,React可能會推遲它,稍後會一次性更新這些元件。React不會保證在setState之後,能夠立刻拿到改變的結果。
以上說明執行setState時,有可能是非同步(大部分情況下)更新元件(包括重新render ui以及及時修改元件this.state)。React為什麼要做成大部分setState是非同步的呢?有哪些情況是進行同步更新元件並且更新this.state的呢?
先說答案:在元件生命週期或React合成事件中,setState是非同步;在setTimeout或者原生dom事件中,setState是同步。
為什麼react大部分情況setState是非同步的呢?假如所有setState是同步的,意味著每執行一次setState時(有可能一個同步程式碼中,多次setState),都重新vnode diff + dom修改,這對效能來說是極為不好的。如果是非同步,則可以把一個同步程式碼中的多個setState合併成一次元件更新。
舉個例子:
var Counter = React.createClass({ getInitialState: function () { return { count: 0 }; }, handleClick: function () { // 同步程式碼中,多次setState最終只會執行一次元件更新(元件更新意味著this.state拿到最新值) this.setState({count: 1, }, (state) => { this.setState({ count : 3}) console.log(this.state, 'next update') // 2 }); this.setState({ count: 2 }); console.log(this.state, 'first') // 0 這就是大家常說的setState是非同步過程,因為執行後元件state(this.state)沒有改變 // 同步表現 setTimeout(() => { this.setState({count: 4}) console.log(this.state, 'setTimeout') // 4 在setTimeout中執行setState,同步渲染ui以及及時更新this.state(同步表現) }, 0) }, render: function () { console.log(this.state, 'render') // 2 return ( <button onClick={this.handleClick}> Click me! Number of clicks: {this.state.count} </button> ); } }); ReactDOM.render( <Counter />, document.getElementById('container') ); // 初始化時 {count: 0} "render" // 單擊按鈕後 embedded:16 {count: 0} "first" embedded:25 {count: 2} "render" embedded:13 {count: 2} "next update" embedded:25 {count: 3} "render" embedded:25 {count: 4} "render" embedded:20 {count: 4} "setTimeout"
react setState是如何實現的呢?非同步更新的原理是什麼呢?(以下原始碼分析基於react15.6)
setState非同步實現
ReactComponent.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } }; enqueueSetState: function(publicInstance, partialState) { // 找到需渲染元件 var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState', ); if (!internalInstance) { return; } // 每次都把新的state,push到佇列中。 // 方便後面一次性更新元件時,聚合成最新的state var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); // 更新 enqueueUpdate(internalInstance); },
//程式碼位於ReactUpdateQueue.js function enqueueUpdate(internalInstance) { ReactUpdates.enqueueUpdate(internalInstance); } //程式碼位於ReactUpdates.js function enqueueUpdate(component) { ensureInjected(); // 未開啟事務流程:開啟事務 + 更新元件 // 在生命週期以及合成事件情況下,isBatchingUpdates=true // 在setTimeout以及原生DOM事件情況下,isBatchingUpdates=false if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } // 已開啟事務流程:放到髒陣列中(元件不更新 + this.state不變),等待更新 dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; } }
以上是setState的關鍵程式碼,batchingStrategy.batchedUpdates裡面用到了事務機制。 setState 本身的方法呼叫是同步的,但是呼叫了setState不標誌這react的 state 立即更新,這個更新是要根據當前環境執行上下文來判斷的,如果處於batchedUpadte的情況下,那麼state的不是當前立馬更新的,而不處於batchedUpadte的情況下,那麼他就有可能立馬更新的。
所以在componentDidMount中呼叫setState並不會立即更新state,因為正處於一個更新流程中,isBatchingUpdates為true,所以只會放入dirtyComponents中等待稍後更新。
合成事件中呼叫setState
dispatchEvent: function (topLevelType, nativeEvent) { // disable了則直接不回撥相關方法 if (!ReactEventListener._enabled) { return; } var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent); try { // 在執行合成事件回撥函式前,都先開啟事務 // 這樣在執行回撥函式里的setState時,都是放入髒陣列時,往後更新 ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } } ReactUpdates.batchedUpdates(callback, a, b, c, d, e) { ensureInjected(); // 執行batchingStrategy.batchedUpdates意味著已開啟事務流 return batchingStrategy.batchedUpdates(callback, a, b, c, d, e); }
更新元件處理state
更新元件時,有對state進行處理
ReactCompositeComponent.updateComponent: function( transaction, prevParentElement, // pre VNode nextParentElement, // next VNode prevUnmaskedContext, nextUnmaskedContext, ) { var inst = this._instance; var prevProps = prevParentElement.props; var nextProps = nextParentElement.props; // componentWillReceiveProps 生命週期 if (willReceive && inst.componentWillReceiveProps) { inst.componentWillReceiveProps(nextProps, nextContext); } // 對在pending佇列中的state,進行merge state,拿到最新state值。 var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = true; // 是否要更新元件,預設是true if (inst.shouldComponentUpdate) { // 如果元件裡有定義 shouldUpdate = inst.shouldComponentUpdate( nextProps, nextState, nextContext, ); } else { // 如果是純元件(PureComponent),淺比較 if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); } } // 是否更新元件,這裡常是使用者最佳化的地方,控制什麼時候React元件什麼時候更新。 // 不設定就是true,子元件都會VDOM比較一遍(意味著子元件沒變時,也會去比較(多餘的操作,所以可以在此最佳化效能),不過浪費的效能是VDOM比較,而不是會改動DOM)。 if (shouldUpdate) { // _performComponentUpdate --> _updateRenderedComponent this._performComponentUpdate( nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext, ); } },
// 合併state,拿到最新值 _processPendingState: function(props, context) { var inst = this._instance; var queue = this._pendingStateQueue; var replace = this._pendingReplaceState; this._pendingReplaceState = false; this._pendingStateQueue = null; // pending佇列中只有0個或1個處理 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 < queue.length; i++) { var partial = queue[i]; Object.assign( nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial, ); } return nextState; },
總結
如果在以下情況下執行setState方法:
生命週期裡
-此時已經開啟了事務,當執行多個state時,所有都是在髒陣列中,沒有同步更新元件,意味著此時元件上的state沒有更新。這也是為什麼上面列印this.state.count會是0合成事件回撥函式里
-下發事件時開啟了事務,回撥函式里執行setState都是放在髒陣列中,同上setTimeout和DOM原生事件裡
,此時沒有開啟事務,直接同步更新元件 + 修改為最新的this.state