對於大多數的React開發者,setState多是最經常使用的API之一。React做爲View層,經過改變data從而引起UI的更新。React不像Vue這種MVVM庫,直接修改data並不能視圖的改變,更新狀態(state)的過程必須使用setState。 javascript
setState的函數簽名以下:java
setState(partialState,callback)
咱們看到setState接受兩個參數,一個是partialState
,它是新的state用來更新以前的state。callback
做爲回調函數,會在更新結束以後執行。舉個常見的例子數組
this.setState({
value: this.state.value + 1
})
上面這個例子執行的結果是將state中value的值增長1。但事實真的如此簡單嗎?咱們看下面的代碼: bash
class Example extends React.Component {
constructor(props) {
super(props);
}
state = {
value: 0
}
render() {
return (
<div> <div>The Value: {this.state.value}</div> <button onClick={::this._addValue}>add Value</button> </div>
);
}
_addValue() {
this.setState({
value: this.state.value + 1
})
this.setState({
value: this.state.value + 1
})
}
}
若是你認爲點擊"addValue"按妞時每次會增長2的話,說明你可能對setState不是很瞭解。事實上若是你真的須要每次增長2的話,你的_addValue
函數應該這麼寫: app
_addValue() {
this.setState((preState,props)=>({
value: preState.value + 1
}))
this.setState((preState,props)=>({
value: preState.value + 1
}))
}
咱們能夠看到其實參數partialState
不只能夠是一個對象,也能夠是一個函數。該函數接受兩個參數: 更新前的state(preState
)與當前的屬性(props),函數返回一個對象用於更新state。爲何會產生這個問題,答案會在後序解答。
其實上面的例子中,若是你真的須要每次增長2的話,你也能夠這麼寫,雖然下面的寫法不是很優美:異步
_addValue() {
setTimeout(()=>{
this.setState({
value: this.state.value + 1
});
this.setState({
value: this.state.value + 1
});
},0)
}
你如今是否眉頭一皺,發現setState並無這麼簡單。
ide
關於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.oopThere 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.
翻譯過來(意譯)至關於:
setState用來設置
state
的子集,永遠都只使用setState更改state
。你應該將this.state
視爲不可變數據。並不能保證this.state會被當即更新,所以在調用這個方法以後訪問
this.state
可能會獲得的是以前的值。不能保證調用setState以後會同步運行,由於它們可能被批量更新,你能夠提供可選的回調函數,在setState真正地完成了以後,回調函數將會被執行。
通篇幾個字眼讓咱們很難辦,不保證、可能,到底何時纔會同步更新,何時纔會異步更新?可能真的須要咱們研究一下。
React組件繼承自React.Component
,而setState是React.Component
的方法,所以對於組件來說setState
屬於其原型方法,首先看setState的定義:
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback);
}
};
咱們首先看setState,首先調用的是this.updater.enqueueSetState
,先明確this.updater
是什麼,在React中每一個組件有擁有一個this.updater
,是用來驅動state
更新的工具對象。當咱們在組件中的構造函數中調用super
時實質調用的就是函數ReactComponent
。其中有:
this.updater = updater || ReactNoopUpdateQueue;
沒有傳入參數updater
參數時,this.updater
的值就是ReactNoopUpdateQueue
。 而ReactNoopUpdateQueue
實際是沒有什麼意義的,只至關於一個初始化的過程。而ReactNoopUpdateQueue.enqueueSetState
主要起到一個在非生產版本中警告(warning)的做用。真正的updater
是在renderer
中注入(inject)的。所以若是你在constructor
中嘗試調用this.helper.isMounted
會返回false
,代表組件並無安裝(mount
),若是你調用setState,也會給出相應的警告。
constructor(props) {
super(props);
//這是指個演示,this.isMounted函數已經被廢棄
console.log(this.updater.isMounted())
this.setState({
value: 1
})
}
上面的警告就是ReactNoopUpdateQueue
中負責打印的。告訴咱們在非安裝或已卸載的組件上是不能使用setState函數的。
在ReactCompositeComponentMixin
中的函數mountComponent
中有下面的語句:
inst.updater = ReactUpdateQueue;
那咱們來看看ReactUpdateQueue
中的enqueueSetState
:
var ReactUpdatedQueue = {
enqueueSetState: function (publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue
|| (internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},
}
咱們經過this.updater.enqueueSetState(this, partialState);
這裏的this
是組件的實例,例如在最開始的例子中,this
指的就是函數Example
的實例(class
實質就是函數function
的語法糖)。以下圖:
經過執行函數
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
咱們獲得的internalInstance
實質就是組件實例的React內部表達,包含了組件實例的內部的一些屬性,例如:
internalInstance
的屬性不少,但咱們須要關注的只有兩個:
_pendingStateQueue
(待更新隊列)與
_pendingCallbacks
(更新回調隊列)。根據代碼
var queue = internalInstance._pendingStateQueue
|| (internalInstance._pendingStateQueue = []);
queue.push(partialState);
若是_pendingStateQueue
的值爲null
,將其賦值爲空數組[]
,並將partialState
放入待更新state隊列_pendingStateQueue
。最後執行enqueueUpdate(internalInstance);
。所以下一步咱們須要研究一下enqueueUpdate
。
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
var ReactUpdates = {
enqueueUpdate: function enqueueUpdate(component) {
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}
}
首先執行的ensureInjected()
其實也是一個保證ReactUpdates.ReactReconcileTransaction
與batchingStrategy
是否存在,不然給出相應的警告,固然上面兩個的做用以後會給出。接下來會根據batchingStrategy.isBatchingUpdates的值作出不一樣的行爲,若是是true
的話,直接將internalInstance
放入dirtyComponents
,不然將執行batchingStrategy.batchedUpdates(enqueueUpdate, component)
。那麼咱們要了解一下batchingStrategy
是幹什麼的。首先看batchingStrategy
的定義:
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
transaction.perform(callback, null, a, b, c, d, e);
}
},
};
batchingStrategy
實質上就是一種批量更新策略,其屬性isBatchingUpdates
表示的是否處於批量更新的過程當中,開始默認值爲false。batchedUpdates
就是執行批量更新的方法。當isBatchingUpdates
爲false
時,執行transaction.perform(callback, null, a, b, c, d, e)
。不然當isBatchingUpdates
爲true
時,直接執行callback
。但在咱們這裏,其實不會執行到這兒,由於當isBatchingUpdates
爲true
時,直接就將component
中放入dirtyComponents
中。關於代碼中的transaction
咱們須要瞭解下React中的事務Transaction。
關於React中的事務Transaction,源碼中給出了下面的ASCII圖:
/** * <pre> * wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ * </pre> */
其實上面的形象的解釋了React中的事務Transaction,React Transaction會給方法包裝一個個wrapper,其中每一個wrapper
都有兩個方法:initialize
與close
。當執行方法時,須要執行事務的perform
方法。perform
方法會首先一次執行wrapper
的initialize
,而後執行函數自己,最後執行wrapper
的close
方法。
定義Transaction須要給構造函數混入Transaction.Mixin,並須要提供一個原型方法getTransactionWrappers
用於返回wrapper數組。下面咱們看下ReactDefaultBatchingStrategy
中的transaction
是如何定義的:
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
},
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
this.reinitializeTransaction();
}
Object.assign(
ReactDefaultBatchingStrategyTransaction.prototype,
Transaction.Mixin,
{
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
}
);
var transaction = new ReactDefaultBatchingStrategyTransaction();
其中wrapperRESET_BATCHED_UPDATES
負責在close
階段重置ReactDefaultBatchingStrategy
的isBatchingUpdates
爲false
。而wrapperFLUSH_BATCHED_UPDATES
負責在close
執行flushBatchedUpdates
。
咱們再次回顧一下更新的過程,若是處於批量更新的過程當中(即isBatchingUpdates爲true
),則直接將組件傳入dirtyComponents
。若是不是的話,開啓批量更新,用事務transaction.perform
執行enqueueUpdate
,這時候isBatchingUpdates
通過上次執行,已是true
,將被直接傳入dirtyComponents
。那麼傳入更新的組件傳入dirtyComponents
以後會發生什麼?
咱們知道,batchedUpdates
是處於一個事務中的,該事務在close
階段作了兩件事,首先是將ReactDefaultBatchingStrategy.isBatchingUpdates
置爲false
,即關閉批量更新的標誌位,第二個就是調用了方法ReactUpdates.flushBatchedUpdates
。flushBatchedUpdates
中會涉及到Virtual DOM到真實DOM的映射,這不是咱們這篇文章的重點(最重要的是我本身也沒有參透這邊的邏輯),這部分咱們只會簡要的介紹流程。
//代碼有省略
var flushBatchedUpdates = function() {
while (dirtyComponents.length) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
//......
}
};
咱們發如今函數flushBatchedUpdates
中是以事務ReactUpdatesFlushTransaction
的方式執行了函數runBatchedUpdates
,追根溯源咱們來看看runBatchedUpdates
幹了什麼。
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
dirtyComponents.sort(mountOrderComparator);
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
//.....
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
//.......
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(
callbacks[j],
component.getPublicInstance()
);
}
}
}
}
首先函數將dirtyComponents
以組件中的_mountOrder
進行了遞增排序,其目的就是保證更新順序,即父組件保證其子組件以前更新。而後在組件中得到setState
完成以後的回調函數,開始執行ReactReconciler.performUpdateIfNecessary
。又得看看這個函數:
performUpdateIfNecessary: function (internalInstance, transaction) {
internalInstance.performUpdateIfNecessary(transaction);
}
performUpdateIfNecessary
執行組件實例的原型方法performUpdateIfNecessary
,咱們再去看看組件實例是如何定義的這個方法:
var ReactCompositeComponentMixin = {
performUpdateIfNecessary: function(transaction) {
//......
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(
transaction,
this._currentElement,
this._currentElement,
this._context,
this._context
);
}
}
}
上面代碼是perfromUpdateIfNecessary
的省略版本,主要調用的其中的this.updateComponent
方法:
updateComponent: function( transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext ) {
var inst = this._instance;
var willReceive = false;
var nextContext;
var nextProps;
// 驗證組件context是否改變
// ......
// 驗證是不是props更新仍是組件state更新
if (prevParentElement === nextParentElement) {
nextProps = nextParentElement.props;
} else {
//存在props的更新
nextProps = this._processProps(nextParentElement.props);
willReceive = true;
}
//根據條件判斷是否調用鉤子函數componentWillReceiveProps
if (willReceive && inst.componentWillReceiveProps) {
inst.componentWillReceiveProps(nextProps, nextContext);
}
//計算新的state
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate =
this._pendingForceUpdate ||
!inst.shouldComponentUpdate ||
inst.shouldComponentUpdate(nextProps, nextState, nextContext);
if (shouldUpdate) {
this._pendingForceUpdate = false;
this._performComponentUpdate(
nextParentElement,
nextProps,
nextState,
nextContext,
transaction,
nextUnmaskedContext
);
} else {
this._currentElement = nextParentElement;
this._context = nextUnmaskedContext;
inst.props = nextProps;
inst.state = nextState;
inst.context = nextContext;
}
}
updateComponent
方法已經作了相關的註釋,其實裏面不只涉及到state的改變致使的從新渲染,還有props的更新致使的從新渲染。在計算新的state
時調用了_processPendingState
:
{
_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 < queue.length; i++) {
var partial = queue[i];
Object.assign(
nextState,
typeof partial === 'function' ?
partial.call(inst, nextState, props, context) :
partial
);
}
return nextState;
}
}
這一部分代碼相對來講不算是很難,replace
是存在是因爲以前被廢棄的APIthis.replaceState
,咱們如今不須要關心這一部分,如今咱們能夠回答剛開始的問題,爲何給setState傳入的參數是函數時,就能夠解決剛開始的例子。
Object.assign( nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial );
若是咱們傳入的是對象
this.setState({value: this.state.value + 1 }); this.setState({value: this.state.value + 1})
咱們如今已經知道,調用setState
是批量更新,那麼第一次調用以後,this.state.value
的值並無改變。兩次更新的value
值實際上是同樣的,因此達不到咱們的目的。可是若是咱們傳遞的是回調函數的形式,那麼狀況就不同了,partial.call(inst, nextState, props, context)
接受的state都是上一輪更新以後的新值,所以能夠達到咱們預期的目的。
_processPendingState
在計算完新的state以後,會執行_performComponentUpdate
:
function _performComponentUpdate( nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext ) {
var inst = this._instance;
var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
var prevProps;
var prevState;
var prevContext;
if (hasComponentDidUpdate) {
prevProps = inst.props;
prevState = inst.state;
prevContext = inst.context;
}
if (inst.componentWillUpdate) {
inst.componentWillUpdate(nextProps, nextState, nextContext);
}
this._currentElement = nextElement;
this._context = unmaskedContext;
inst.props = nextProps;
inst.state = nextState;
inst.context = nextContext;
this._updateRenderedComponent(transaction, unmaskedContext);
if (hasComponentDidUpdate) {
transaction.getReactMountReady().enqueue(
inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
inst
);
}
}
咱們能夠看到,這部份內容涉及到了幾方面內容,首先在更新前調用了鉤子函數componentWillUpdate
,而後更新了組件的屬性(props、state、context),執行函數_updateRenderedComponent
(這部分涉及到render
函數的調用和相應的DOM更新,咱們不作分析),最後再次執行鉤子函數componentDidUpdate
。
到目前爲止,咱們已經基本介紹完了setState的更新過程,只剩一個部分沒有介紹,那就是setState執行結束以後的回調函數。咱們知道,setState函數中若是存在callback,則會有:
if (callback) {
this.updater.enqueueCallback(this, callback);
}
call函數會被傳遞給this.updater
的函數enqueueCallback
,而後很是相似於setState,callback
會存儲在組件內部實例中的_pendingCallbacks
屬性之中。咱們知道,回調函數必需要setState真正完成以後纔會調用,那麼在代碼中是怎麼實現的。你們還記得在函數flushBatchedUpdates
中有一個事務ReactUpdatesFlushTransaction
:
//代碼有省略
var flushBatchedUpdates = function() {
while (dirtyComponents.length) {
if (dirtyComponents.length) {
//從事務pool中得到事務實例
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
//釋放實例
ReactUpdatesFlushTransaction.release(transaction);
}
//......
}
};
咱們如今看看ReactUpdatesFlushTransaction
的wrapper是怎麼定義的:
var UPDATE_QUEUEING = {
initialize: function() {
this.callbackQueue.reset();
},
close: function() {
this.callbackQueue.notifyAll();
},
};
咱們看到在事務的close
階段定義了this.callbackQueue.notifyAll()
,即執行了回調函數,經過這種方法就能保證回調函數必定是在setState真正完成以後才執行的。到此爲止咱們基本已經解釋了setState大體的流程是怎樣的,可是咱們仍是沒有回答以前的一個問題,爲何下面的兩種代碼會產生不一樣的狀況:
//未按預期執行
_addValue() {
this.setState({
value: this.state.value + 1
})
this.setState({
value: this.state.value + 1
})
}
//按預期執行
_addValue() {
setTimeout(()=>{
this.setState({
value: this.state.value + 1
});
this.setState({
value: this.state.value + 1
});
},0)
}
這個問題,其實真的要追本溯源地去講,是比較複雜的,咱們簡要介紹一下。在第一種狀況下,若是打斷點追蹤你會發現,在第一次執行setState前,已經觸發了一個 batchedUpdates,等到執行setState時已經處於一個較大的事務,所以兩個setState都是會被批量更新的(至關於異步更新的過程,thi.state.value值並無當即改變),執行setState只不過是將二者的partialState
傳入dirtyComponents
,最後再經過事務的close
階段的flushBatchedUpdates
方法去執行從新渲染。可是經過setTimeout
函數的包裝,兩次setState都會在click觸發的批量更新batchedUpdates
結束以後執行,這兩次setState會觸發兩次批量更新batchedUpdates,固然也會執行兩個事務以及函數flushBatchedUpdates
,這就至關於一個同步更新的過程,天然能夠達到咱們的目的,這也就解釋了爲何React文檔中既沒有說setState是同步更新或者是異步更新,只是模糊地說到,setState並不保證同步更新。 這篇文章對setState的介紹也是比較淺顯的,可是但願能起到一個拋磚迎玉的做用。setState之因此須要會採用一個批量更新的策略,其目的也是爲了優化更新性能。但對於日常的使用中,雖然咱們不會關心或者涉及到這個問題,可是咱們仍然可使用React開發出高性能的應用,我想這也就是咱們喜歡React的緣由:簡單、高效!