摘要:下面是的源碼如其名,是一個隊列操作,將要變更的統一插入隊列,待一一處理。只有控制下的事件周期,會執行切換狀態,保證批量操作能被截獲并插入堆棧。也就是源碼中的。他在初始被賦值為,也就是以下兩個對象,在源碼被稱作為。本文參考源碼版本為。
React setState
不知道什么時候開始,很多人開始認為setState是異步操作,所謂的異步操作,就是我們在執行了setState之后,立即通過this.state.xxx不能拿到更新之后的值。這樣的認知其實有一種先入為主的意識,也許是受到很多不知名博主的不科學言論導致的錯誤認知,也有可能是日常開發過程中積累的經驗。畢竟大部分開發寫setState這樣的方法,都是在組件的生命周期(如componentDidMount、componentWillMount)中,或者react的事件處理機制中,這種教科書式的寫代碼方式,基本不會碰到有數據異常。
雖然官方文檔對setState這種同步行為語焉不詳,但是我們可以發現某些情況下,setState是真的可以同步獲取數據的。通過本文我們可以了解react這方面的工作原理,對于我們的思考開發方案,解決疑難問題,避免不必要的錯誤,也許會有不少幫助。
我們先來說結論:
在React中,如果是由React引發的事件處理(比如通過onClick引發的事件處理,componentWillMount等生命周期),調用setState不會同步更新this.state;除此之外的setState調用會同步執行this.state。所謂“除此之外”,指的是繞過React通過addEventListener直接添加的事件處理函數,還有通過setTimeout/setInterval產生的異步調用。
不想看長篇大論的同學,到這里就可以結束了。想了解原理的同學請繼續參觀。。
用過angular框架的同學也許記得angular的代碼模式中有一個$timeout這樣的調用方法,和setTimeout功能基本一致,但是setTimeout卻不能實時觸發UI的更新。這是因為$timeout比setTimeout添加了對UI更新(臟檢查)的處理,在延時結束后立即調用更新方法更新UI的渲染。同樣的道理,我們必須使用react指定的方式更新state才能同步UI的渲染,因為react控制下的事件會同步處理UI的更新。而直接使用this.state.xxx = xxx這樣的方式僅僅改變了數據,沒有改變UI,這就不是React倡導的reactive programing了。
實際上,在react的源碼中我們會發現,大部分react控制下的事件或生命周期,會調用batchedUpdates(查看如下代碼)。這個方法會觸發component渲染的狀態isBatchingUpdates。同樣的,react的事件監聽機制會觸發batchedUpdates方法,同樣會將isBatchingUpdates狀態置為true。
// 更新狀態 batchingStrategy.batchedUpdates(method, component);
在組件渲染狀態isBatchingUpdates中,任何的setState都不會觸發更新,而是進入隊列。除此之外,通過setTimeout/setInterval產生的異步調用是可以同步更新state的。這樣的講解比較抽象,我們可以直接根據以下源碼開始理解。
setState下面我們來看下setState在源碼中的定義:
/** * Sets a subset of the state. Always use this to mutate * state. You should treat `this.state` as immutable. * * There is no guarantee that `this.state` will be immediately updated, so * accessing `this.state` after calling this method may return the old value. * * There is no guarantee that calls to `setState` will run synchronously, * as they may eventually be batched together. You can provide an optional * callback that will be executed when the call to setState is actually * completed. * * When a function is provided to setState, it will be called at some point in * the future (not synchronously). It will be called with the up to date * component arguments (state, props, context). These values can be different * from this.* because your function may be called after receiveProps but before * shouldComponentUpdate, and this new state, props, and context will not yet be * assigned to this. * * @param {object|function} partialState Next partial state or function to * produce next partial state to be merged with current state. * @param {?function} callback Called after state is updated. * @final * @protected */ ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, "setState"); } };
根據源碼中的注釋,有這么一句話。
There is no guarantee that this.state will be immediately updated, so accessing this.state after calling this method may return the old value.
大概意思就是setState不能確保實時更新state,官方從來沒有說過setState是一種異步操作,但也沒有否認,只是告訴我們什么時候會觸發同步操作,什么時候是異步操作。所以我們工作中千萬不要被一些民間偏方蒙蔽雙眼,多看看源代碼,發現原理的同時,還可以發現很多好玩的東西,開源庫的好處就是在于我們能在源碼中發現真理。
我們在源碼的這段注釋里也能看到setState的一些有趣玩法,比如
// 在回調中操作更新后的state this.setState({ count: 1 }, function () { console.log("# next State", this.state); }); // 以非對象的形式操作 this.setState((state, props, context) => { return { count: state.count + 1 } });
回到正題,源碼中setState執行了this.updater.enqueueSetState方法和this.updater.enqueueCallback方法 ,暫且不論enqueueCallback,我們關注下enqueueSetState的作用。
enqueueSetState下面是enqueueSetState的源碼:
/** * Sets a subset of the state. This only exists because _pendingState is * internal. This provides a merging strategy that is not available to deep * properties which is confusing. TODO: Expose pendingState or don"t use it * during the merge. * * @param {ReactClass} publicInstance The instance that should rerender. * @param {object} partialState Next partial state to be merged with state. * @internal */ enqueueSetState: function (publicInstance, partialState) { var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, "setState"); if (!internalInstance) { return; } var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); }
enqueueSetState如其名,是一個隊列操作,將要變更的state統一插入隊列,待一一處理。隊列數據_pengdingStateQueue會掛載在一個組件對象上internalInstance,對于internalInstance想要了解下的同學,可以參考下react源碼中的ReactInstanceMap這個概念。
隊列操作完成之后,就開始真正的更新操作了。
enqueueUpdate更新方法enqueueUpdate的源碼如下:
/** * Mark a component as needing a rerender, adding an optional callback to a * list of functions which will be executed once the rerender occurs. */ function enqueueUpdate(component) { ensureInjected(); // Various parts of our code (such as ReactCompositeComponent"s // _renderValidatedComponent) assume that calls to render aren"t nested; // verify that that"s the case. (This is called by each top-level update // function, like setProps, setState, forceUpdate, etc.; creation and // destruction of top-level components is guarded in ReactMount.) if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); }
第一次執行setState的時候,可以進入if語句,遇到里面的return語句,終止執行。如果不是正處于創建或更新組件階段,則處理update事務。
第二次執行setState的時候,進入不了if語句,將組件放入dirtyComponents。如果正在創建或更新組件,則暫且先不處理update,只是將組件放在dirtyComponents數組中。
enqueueUpdate包含了React避免重復render的邏輯。參考源碼中batchedUpdates的調用情況,mountComponent和updateComponent方法在執行的最開始,會調用到batchedUpdates進行批處理更新,這些是react實例的生命周期,此時會將isBatchingUpdates設置為true,也就是將狀態標記為現在正處于更新階段了。之后React以事務的方式處理組件update,事務處理完后會調用wrapper.close(), 而TRANSACTION_WRAPPERS中包含了RESET_BATCHED_UPDATES這個wrapper,故最終會調用RESET_BATCHED_UPDATES.close(), 它最終會將isBatchingUpdates設置為false。
聽不懂?聽不懂沒關系。。我們會一句句剖析。
enqueueUpdate和batchingStrategy的概念我們放一起考慮。
batchingStrategy簡單直譯叫做批量處理策略。這個是React處理批量state操作時的精髓,源碼如下:
var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, /** * Call the provided function in a context within which calls to `setState` * and friends are batched such that components aren"t updated unnecessarily. */ 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) { callback(a, b, c, d, e); } else { transaction.perform(callback, null, a, b, c, d, e); } } };
如enqueueUpdate源碼中所述,每次執行更新前,會預先判斷isBatchingUpdates是否處理批量更新狀態,如我們常見的周期諸如componentWillMount、componentDidMount,都是處于isBatchingUpdates的批量更新狀態,此時執行的setState操作,不會進入if語句執行update,而是進入dirtyComponents的堆棧中。
這就是文章開頭所說的栗子,為什么setTimeout執行的setState會同步更新state,而react生命周期中執行的setState只能異步更新的原因。只有react控制下的事件周期,會執行batchedUpdates切換isBatchingUpdates狀態,保證批量操作能被截獲并插入堆棧。其他事件都和同步執行update方法無異。
執行batchedUpdates之后,會立即將isBatchingUpdates賦值為true,表明此時即將進入更新狀態,所有之后的setState進入隊列等待。
這里我們以普通的setTimeout為例,執行一次更新。業務代如下:
setTimeout(function () { this.setState({ count: this.state.count + 1 }); }, 0);
執行時isBatchingUpdates默認是false,所以當我們執行到batchedUpdates這一步的時候,源碼中alreadyBatchingUpdates被賦值為false,我們會跳過if進入else條件,執行下一階段transaction.perform。
transaction.performperform為我們執行了UI更新的第一步預操作。這里我們會執行一系列更新初始化操作和更新狀態的關閉。該方法做了try-catch控制,大量數據操作有可能引發錯誤exception,perform方法在這里對錯誤做了截獲控制。
/** * Executes the function within a safety window. Use this for the top level * methods that result in large amounts of computation/mutations that would * need to be safety checked. The optional arguments helps prevent the need * to bind in many cases. * * @param {function} method Member of scope to call. * @param {Object} scope Scope to invoke from. * @param {Object?=} a Argument to pass to the method. * @param {Object?=} b Argument to pass to the method. * @param {Object?=} c Argument to pass to the method. * @param {Object?=} d Argument to pass to the method. * @param {Object?=} e Argument to pass to the method. * @param {Object?=} f Argument to pass to the method. * * @return {*} Return value from `method`. */ perform: function (method, scope, a, b, c, d, e, f) { !!this.isInTransaction() ? "development" !== "production" ? invariant(false, "Transaction.perform(...): Cannot initialize a transaction when there " + "is already an outstanding transaction.") : invariant(false) : void 0; var errorThrown; var ret; try { this._isInTransaction = true; // Catching errors makes debugging more difficult, so we start with // errorThrown set to true before setting it to false after calling // close -- if it"s still set to true in the finally block, it means // one of these calls threw. errorThrown = true; this.initializeAll(0); ret = method.call(scope, a, b, c, d, e, f); errorThrown = false; } finally { try { if (errorThrown) { // If `method` throws, prefer to show that stack trace over any thrown // by invoking `closeAll`. try { this.closeAll(0); } catch (err) {} } else { // Since `method` didn"t throw, we don"t want to silence the exception // here. this.closeAll(0); } } finally { this._isInTransaction = false; } } return ret; }
源碼中執行了一些錯誤的預判,最終我們真正執行的是closeAll方法。關于state的數據更新,從close開始。
close/** * Invokes each of `this.transactionWrappers.close[i]` functions, passing into * them the respective return values of `this.transactionWrappers.init[i]` * (`close`rs that correspond to initializers that failed will not be * invoked). */ closeAll: function (startIndex) { !this.isInTransaction() ? "development" !== "production" ? invariant(false, "Transaction.closeAll(): Cannot close transaction when none are open.") : invariant(false) : void 0; var transactionWrappers = this.transactionWrappers; for (var i = startIndex; i < transactionWrappers.length; i++) { var wrapper = transactionWrappers[i]; var initData = this.wrapperInitData[i]; var errorThrown; try { // Catching errors makes debugging more difficult, so we start with // errorThrown set to true before setting it to false after calling // close -- if it"s still set to true in the finally block, it means // wrapper.close threw. errorThrown = true; if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) { wrapper.close.call(this, initData); } errorThrown = false; } finally { if (errorThrown) { // The closer for wrapper i threw an error; close the remaining // wrappers but silence any exceptions from them to ensure that the // first error is the one to bubble up. try { this.closeAll(i + 1); } catch (e) {} } } } this.wrapperInitData.length = 0; }
在介紹close之前,我們先了解下兩個對象。也就是源碼中的this.transactionWrappers。他在初始被賦值為[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES],也就是以下兩個對象,在源碼被稱作為wrapper。
var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false; } }; var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates) };
源碼中我們看到closeAll執行了一次for循環,并執行了每個wrapper的close方法。
RESET_BATCHED_UPDATES的close方法很簡單,把isBatchingUpdates更新中這個狀態做了一個close的操作,也就是賦值為false,表明本次批量更新已結束。
FLUSH_BATCHED_UPDATES的close方法執行的是flushBatchedUpdates方法。
flushBatchedUpdatesvar flushBatchedUpdates = function () { // ReactUpdatesFlushTransaction"s wrappers will clear the dirtyComponents // array and perform any updates enqueued by mount-ready handlers (i.e., // componentDidUpdate) but we need to check here too in order to catch // updates enqueued by setState callbacks and asap calls. while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); transaction.perform(runBatchedUpdates, null, transaction); ReactUpdatesFlushTransaction.release(transaction); } if (asapEnqueued) { asapEnqueued = false; var queue = asapCallbackQueue; asapCallbackQueue = CallbackQueue.getPooled(); queue.notifyAll(); CallbackQueue.release(queue); } } };
我們暫且不論asap是什么,可以看到flushBatchedUpdates做的是對dirtyComponents的批量處理操作,對于隊列中的每個component執行perform更新。這些更新都會執行真正的更新方法runBatchedUpdates。
function runBatchedUpdates(transaction) { var len = transaction.dirtyComponentsLength; !(len === dirtyComponents.length) ? "development" !== "production" ? invariant(false, "Expected flush transaction"s stored dirty-components length (%s) to " + "match dirty-components array length (%s).", len, dirtyComponents.length) : invariant(false) : void 0; // Since reconciling a component higher in the owner hierarchy usually (not // always -- see shouldComponentUpdate()) will reconcile children, reconcile // them before their children by sorting the array. dirtyComponents.sort(mountOrderComparator); for (var i = 0; i < len; i++) { // If a component is unmounted before pending changes apply, it will still // be here, but we assume that it has cleared its _pendingCallbacks and // that performUpdateIfNecessary is a noop. var component = dirtyComponents[i]; // If performUpdateIfNecessary happens to enqueue any new updates, we // shouldn"t execute the callbacks until the next render happens, so // stash the callbacks first var callbacks = component._pendingCallbacks; component._pendingCallbacks = null; var markerName; if (ReactFeatureFlags.logTopLevelRenders) { var namedComponent = component; // Duck type TopLevelWrapper. This is probably always true. if (component._currentElement.props === component._renderedComponent._currentElement) { namedComponent = component._renderedComponent; } markerName = "React update: " + namedComponent.getName(); console.time(markerName); } ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction); if (markerName) { console.timeEnd(markerName); } if (callbacks) { for (var j = 0; j < callbacks.length; j++) { transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance()); } } } }
runBatchedUpdates中的核心處理是ReactReconciler.performUpdateIfNecessary。
/** * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate` * is set, update the component. * * @param {ReactReconcileTransaction} transaction * @internal */ performUpdateIfNecessary: function (transaction) { if (this._pendingElement != null) { ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context); } if (this._pendingStateQueue !== null || this._pendingForceUpdate) { this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); } }
在這里我們終于又看到了我們熟悉的_pendingStateQueue,還記得這是什么嗎?是的,這就是state的更新隊列,performUpdateIfNecessary做了隊列的特殊判斷,避免導致錯誤更新。
接下來的這段代碼是updateComponent,源碼內容比較長,但是我們可以看到很多熟知的生命周期方法的身影,比如說componentWillReceiveProps和shouldComponentUpdate,做了component的更新判斷。
這部分方法統一歸屬于ReactCompositeComponentMixin模塊,有興趣了解整個生命周期的同學可以參考下源碼中的該模塊源碼,這里我們不再擴展,會繼續講解state的更新過程。updateComponent
/** * Perform an update to a mounted component. The componentWillReceiveProps and * shouldComponentUpdate methods are called, then (assuming the update isn"t * skipped) the remaining update lifecycle methods are called and the DOM * representation is updated. * * By default, this implements React"s rendering and reconciliation algorithm. * Sophisticated clients may wish to override this. * * @param {ReactReconcileTransaction} transaction * @param {ReactElement} prevParentElement * @param {ReactElement} nextParentElement * @internal * @overridable */ updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) { var inst = this._instance; var willReceive = false; var nextContext; var nextProps; // Determine if the context has changed or not if (this._context === nextUnmaskedContext) { nextContext = inst.context; } else { nextContext = this._processContext(nextUnmaskedContext); willReceive = true; } // Distinguish between a props update versus a simple state update if (prevParentElement === nextParentElement) { // Skip checking prop types again -- we don"t read inst.props to avoid // warning for DOM component props in this upgrade nextProps = nextParentElement.props; } else { nextProps = this._processProps(nextParentElement.props); willReceive = true; } // An update here will schedule an update but immediately set // _pendingStateQueue which will ensure that any state updates gets // immediately reconciled instead of waiting for the next batch. if (willReceive && inst.componentWillReceiveProps) { inst.componentWillReceiveProps(nextProps, nextContext); } var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext); if ("development" !== "production") { "development" !== "production" ? warning(shouldUpdate !== undefined, "%s.shouldComponentUpdate(): Returned undefined instead of a " + "boolean value. Make sure to return true or false.", this.getName() || "ReactCompositeComponent") : void 0; } if (shouldUpdate) { this._pendingForceUpdate = false; // Will set `this.props`, `this.state` and `this.context`. this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext); } else { // If it"s determined that a component should not update, we still want // to set props and state but we shortcut the rest of the update. this._currentElement = nextParentElement; this._context = nextUnmaskedContext; inst.props = nextProps; inst.state = nextState; inst.context = nextContext; } }
跳過除了state的其他源碼部分,我們可以看到該方法中仍然嵌套了一段對state的更新方法,這個方法就是state更新的終點_processPendingState。
_processPendingState為什么對state中的同一屬性做多次setState處理,不會得到多次更新?比如
this.setState({ count: count++ }); this.set
那是因為源碼中的多個nextState的更新,只做了一次assign操作,如下源碼請查看:
_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]; _assign(nextState, typeof partial === "function" ? partial.call(inst, nextState, props, context) : partial); } return nextState; }
有人說,React抽象來說,就是一個公式
UI=f(state).
的確如此,一個簡單的setState執行過程,內部暗藏了這么深的玄機,經歷多個模塊的處理,經歷多個錯誤處理機制以及對數據邊界的判斷,保證了一次更新的正常進行。同時我們也發現了為什么setState的操作不能簡單的說作是一個異步操作,大家應該在文章中已經找到了答案。
對其他react深層的理解,感興趣的同學可以多多參考下源碼。本文參考react源碼版本為15.0.1。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108785.html
摘要:只涉及了,其他均沒有自己實現。這種組件的復用性是最強的。所以會新建,只繼承的原型,不包括,以此來節省內存。 showImg(https://segmentfault.com/img/remote/1460000019783989); 一、React.Component() GitHub:https://github.com/AttackXiaoJinJin/reactExplain/...
摘要:首先是創建了一個構造函數,他的原型指到的原型然后創建了一個加上了和一樣的屬性這里為啥不用。的原型指向的實例修改原型的屬性使其正確指向的構造函數,并掛一個的屬性。 每次都信誓旦旦的給自己立下要好好學習react源碼的flag,結果都是因為某個地方卡住了,或是其他原因沒看多少就放棄了。這次又給自己立個flag-堅持看完react源碼。為了敦促自己,特開設這樣一個專欄來記錄自己的學習歷程,這...
摘要:新的值回調函數。官方注解是給組件做個標記需要重新渲染,并且將可選的回調函數添加到函數列表中,這些函數將在重新渲染的時候執行。一共做了兩件事一是通過執行方法來更新組件二是若方法傳入了回調函數則將回調函數存入隊列。 Q1 setState改變狀態之后,不會立即更新state值。所以,如果改變state值,react是什么時候進行組件的更新呢?setState()到底做了一些什么呢? A1 ...
摘要:就是,如果你不了解這個的話可以閱讀下相關文檔,是應用初始化時就會生成的一個變量,值也是,并且這個值不會在后期再被改變。這是我的剖析 React 源碼的第三篇文章,如果你沒有閱讀過之前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀源碼。 文章相關資料 React 16.8.6 源碼中文注釋,這個鏈接是文章的核心,文中的具體代碼及代碼行數都是依托于這個倉庫 熱身...
閱讀 817·2021-10-13 09:39
閱讀 3697·2021-10-12 10:12
閱讀 1741·2021-08-13 15:07
閱讀 1005·2019-08-29 15:31
閱讀 2883·2019-08-26 13:25
閱讀 1776·2019-08-23 18:38
閱讀 1878·2019-08-23 18:25
閱讀 1857·2019-08-23 17:20