這個問題相信是大部分的人剛開始學習React
的時候,第一個碰到的問題,對我來講,學習的教程裏就只是告訴我直接使用setState
是異步的,可是在一些好比setTimeout
這樣的異步方法裏,它是同步的。數組
我那時候就很疑惑,雖然想要一探究竟,可是爲了儘快上手React
,仍是以應用優先,留到以後的源碼學習時,再來深刻了解一下。緩存
本文的內容就是從源碼層面來分析setState
究竟是同步仍是異步的。由於如今作的項目其實都是使用hooks
在維護數據狀態,對於class
使用特別少,因此我並不會太過深究底層的渲染原理,重點只是在於爲何setState
有的時候表現是同步,有的時候表現是異步。性能優化
export default class App extends React.Component{
state = {
num: 0
}
add = () => {
console.log('add前', this.state.num)
this.setState({
num: this.state.num + 1
});
console.log('add後', this.state.num)
}
add3 = () => {
console.log('add3前', this.state.num)
this.setState({
num: this.state.num + 1
});
this.setState({
num: this.state.num + 1
});
this.setState({
num: this.state.num + 1
});
console.log('add3後', this.state.num)
}
reduce = () => {
setTimeout(() => {
console.log('reduce前', this.state.num)
this.setState({
num: this.state.num - 1
});
console.log('reduce後', this.state.num)
},0);
}
render () {
return <div> <button onClick={this.add}>點擊加1</button> <button onClick={this.add3}>點擊加3次</button> <button onClick={this.reduce}>點我減1</button> </div>
}
}
複製代碼
按順序依次點擊這三個按鈕,咱們來看下控制檯打印出來的內容。markdown
這個結果,對於有點經驗React
開發者來講很簡單。app
按照這個例子來看,setState
是一個異步的方法,執行完成以後,數據不會立刻修改,會等到後續某個時刻才進行變化。屢次調用setState
,只會執行最新的那個事件。在異步的方法中,它會有同步的特性。異步
咱們先不着急下結論,咱們深刻setState
的流程中去找結論。ide
出於性能優化的須要,一次setState
是不會觸發一個完整的更新流程的,在一個同步的代碼運行中,每次執行一個setState
,React
會把它塞進一個隊列裏,等時機成熟,再把「攢起來」的state
結果作合併,最後只針對最新的state
值走一次更新流程。這個過程,叫做批量更新。函數
這樣子,就算咱們代碼寫的再爛,好比寫了一個循環100次的方法,每次都會調用一個setState
,也不會致使頻繁的re-render
形成頁面的卡頓。性能
這個原理,解釋了上面第一個按鈕以及第二個按鈕的現象。學習
這裏的問題就一個,爲何setTimeout
能夠將setState
的執行順序從異步變爲同步?
咱們來看看setState
的源碼
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
複製代碼
不考慮callback
回調,這裏其實就是觸發了一個enqueueSetState
方法。
enqueueSetState: function (publicInstance, partialState) {
// 根據 this 拿到對應的組件實例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 這個 queue 對應的就是一個組件實例的 state 數組
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate 用來處理當前的組件實例
enqueueUpdate(internalInstance);
}
複製代碼
這個方法就是剛纔說的,把state
的修改放進隊列中。而後使用enqueueUpdate
來處理將要更新的組件實例。再來看看enqueueUpdate
方法。
function enqueueUpdate(component) {
ensureInjected();
// 注意這一句是問題的關鍵,isBatchingUpdates標識着當前是否處於批量建立/更新組件的階段
if (!batchingStrategy.isBatchingUpdates) {
// 若當前沒有處於批量建立/更新組件的階段,則當即更新組件
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 不然,先把組件塞入 dirtyComponents 隊列裏,讓它「再等等」
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
複製代碼
這裏重點關注一個對象,batchingStrategy
(React
內部專門用於管控批量更新的對象),它的屬性isBatchingUpdates
直接決定了當下是否要走更新流程,仍是應該等等。
每當React
調用batchedUpdate
去執行更新動做時,會先把這個鎖給「鎖上」(置爲true
),代表「如今正處於批量更新過程當中」。當鎖被「鎖上」的時候,任何須要更新的組件都只能暫時進入dirtyComponents
裏排隊等候下一次的批量更新。
var ReactDefaultBatchingStrategy = {
// 全局惟一的鎖標識
isBatchingUpdates: false,
// 發起更新動做的方法
batchedUpdates: function(callback, a, b, c, d, e) {
// 緩存鎖變量
var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
// 把鎖「鎖上」
ReactDefaultBatchingStrategy. isBatchingUpdates = true
if (alreadyBatchingStrategy) {
callback(a, b, c, d, e)
} else {
// 啓動事務,將 callback 放進事務裏執行
transaction.perform(callback, null, a, b, c, d, e)
}
}
}
複製代碼
這裏,咱們還須要瞭解React
中的Transaction
(事務) 機制。
Transaction
在React
源碼中表現爲一個核心類,Transaction
能夠建立一個黑盒,該黑盒可以封裝任何的方法。所以,那些須要在函數運行前、後運行的方法能夠經過此方法封裝(即便函數運行中有異常拋出,這些固定的方法仍可運行),實例化Transaction
時只需提供相關的方法便可。
* <pre> * wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ * </pre>
複製代碼
Transaction
就像是一個「殼子」,它首先會將目標函數用wrapper
(一組initialize
及close
方法稱爲一個wrapper
) 封裝起來,同時須要使用Transaction
類暴露的perform
方法去執行它。如上面的註釋所示,在anyMethod
執行以前,perform
會先執行全部 wrapper
的initialize
方法,執行完後,再執行全部wrapper
的close
方法。這就是React
中的事務機制。
結合咱們剛纔的點擊事件,事件實際上是做爲一個callback
回調函數在事務中調用的,調用以前,批量更新策略事務會把isBatchingUpdates
置爲true
,而後執行callback
方法,執行完畢以後,把isBatchingUpdates
置爲false
,而後再循環全部的dirtyComponents
調用updateComponent
更新組件。
因此剛纔的點擊事件,其實能夠這樣理解
add = () => {
// 進來先鎖上
isBatchingUpdates = true
console.log('add前', this.state.num)
this.setState({
num: this.state.num + 1
});
console.log('add後', this.state.num)
// 執行完函數再放開
isBatchingUpdates = false
}
複製代碼
這種狀況下,setState
是異步的。
咱們再來看看setTimeout
的狀況
reduce = () => {
// 進來先鎖上
isBatchingUpdates = true
setTimeout(() => {
console.log('reduce前的', this.state.num)
this.setState({
num: this.state.num - 1
});
console.log('reduce後的', this.state.num)
},0);
// 執行完函數再放開
isBatchingUpdates = false
}
複製代碼
由於setTimeout
是在以後的宏任務中執行的,因此這時候運行的setState
,isBatchingUpdates
已經被置爲false
了,它會當即執行更新,因此具有了同步的特性。setState 並非具有同步這種特性,只是在某些特殊的執行順序下,脫離了異步的控制
setState
並非單純同步/異步的,它的表現會因調用場景的不一樣而不一樣:在React
鉤子函數及合成事件中,它表現爲異步;而在 setTimeout
、setInterval
等函數中,包括在DOM
原生事件中,它都表現爲同步。這種差別,本質上是React
事務機制和批量更新機制的工做方式來決定的。