
壹 ❀ 引
了解react
的同學都知道,react
遵守渲染公式UI=Render(state)
,狀態決定了組件UI最終渲染的樣子(props
也可以理解為外部傳入的狀態),由此可見state
對於react
的重要性。而在實際使用中,若我們想修改狀態必須得借用APIsetState
,也只有通過此方法修改狀態才能順利觸發react
下次render
,那么對於一個使用如此高頻的方法你了解它多少呢?
這里我們可以先拋出幾個問題:
setState
是同步還是異步?- 什么情況下同步?什么情況下異步?
setState
批量合並是指只執行最后一次嗎?比如執行了3次,第1,2次到底有沒有執行?- 為什么要將
setState
設計成異步?這樣設計的好處是什么?
設想一下,上述問題如果在面試中遇到該如何作答?那就讓我們帶着問題出發,另外,文中所有例子強烈建議本地跑一跑,加深對於概念的理解,那么本文開始。
貳 ❀ setState中的同步與異步
貳 ❀ 壹 updater為對象時的異步情況
setState
接受一個帶有形式參數的 updater
函數(也可能直接是一個對象)與一個回調callback
(可選)。
setState(updater, [callback])
官方明確表示,setState
對於this.state
並不是立刻更新,若在調用setState
后想要立刻獲取到最新的this.state
,那么建議在setState
的callback
或者聲明周期componentDidUpdate
中獲取,比如:
class Echo extends React.Component {
state = {
num: 1
}
componentDidUpdate() {
console.log(this.state.num);//2
}
handleOnClick = () => {
this.setState({ num: this.state.num + 1 }, () => {
console.log(this.state.num);//2
});
console.log(this.state.num);//1
}
render() {
return (
<>
<div>{this.state.num}</div>
<button onClick={this.handleOnClick}>加1</button>
</>
)
}
}
其實既然官方特意強調在callback
中獲取最新的this.state
,那就已經說明存在某些地方拿不到最新的this.state
的情況,比如上述代碼中setState
后我們立刻讀取sum
,可以發現num
還是1,那么到這里我們可以得知setState
對於this.state
的更新確實是異步。
問題來了,react
為什么將setState
設計成異步呢?設想下我們有如下這種場景:
class Echo extends React.Component {
state = {
num: 1
}
componentDidUpdate() {
console.log(this.state.num);//2
}
handleOnClick = () => {
this.setState({
num: this.state.num + 1
}, () => {
console.log(this.state.num)//2
});
this.setState({
num: this.state.num + 1
}, () => {
console.log(this.state.num)//2
});
console.log(this.state.num);//1
}
render() {
return (
<>
<div>{this.state.num}</div>
<button onClick={this.handleOnClick}>加1</button>
</>
)
}
}
當點擊按鈕,我們需要連着兩次執行setState
,那么react
會幫我們修改兩次this.state
然后重新render
兩次嗎?很明顯並不是,react
會批量合並多次setState
操作,上述例子num
最終是2,且render
在點擊后只會渲染一次。
React
在開始重新渲染之前, 會有意地進行"等待",直到所有在組件的事件處理函數內調用的 setState()
都完成之后再做最終的this.state
變更,這樣可以通過避免不必要的重新渲染來提升性能。
貳 ❀ 貳 updater為函數時的異步情況
突然奇想,上述代碼的需求有了些許變更,我們還是在點擊后執行兩次setState
,但我預期最終的sum
是3,如何做到呢?別忘了前面我們對於setState
的語法介紹,本質上updater
是一個接受最新state
與最新props
並用於返回你用來更新this.state
的函數:
// 這里可以拿到最新的state與props,注意,是最新的state,而不是最新的this.state
(state, props) => stateChange
函數寫法能讓我們拿到立刻變更后的state
,因此我們可以來看看這個例子:
class Echo extends React.Component {
state = {
num: 1
}
componentDidUpdate() {
console.log('我是更新完成后的this.state',this.state.num);
}
handleOnClick = () => {
this.setState((state, props) => {
console.log('第一次調用,我是最新的state',state.num)
console.log('第一次調用,我是當前的this.state',this.state.num)
// 注意,這里用的是state,不是this.state
return { num: state.num + 1 };
}, () => {
console.log('第一次調用,我是調用完成后的this.state',this.state.num)
});
this.setState((state, preProps) => {
console.log('第二次調用,我是最新的state',state.num)
console.log('第二次調用,我是當前的this.state',this.state.num)
return { num: state.num + 1 };
}, () => {
console.log('第二次調用,我是調用完成后的this.state',this.state.num)
});
console.log('我用於檢驗異步,此時拿不到最新的this.state',this.state.num);//1
}
render() {
console.log('用於檢驗render了幾次');
return (
<>
<div>{this.state.num}</div>
<button onClick={this.handleOnClick}>加1</button>
</>
)
}
}
請問每次setState
時的state
與this.state
是多少,更新完成后最終的this.state
是多少?render
會執行幾次呢?先思考下,答案如下:

最終this.state
是3,且每次setState
中拿到的state
(注意不是this.state
)都是我們預期修改后的,而且根據調用順序來看,雖然確實執行了多次setState
,但最終對於this.state
的修改只有一次,且render
只執行了一次,這種情況下react
依舊做了批量合並處理。
貳 ❀ 叄 批量合並是只執行最后一次setState嗎?
在上述例子中,我們在setState
執行了多次this.state.num+1
的操作,但最后this.state.num
是2,那么請問,所謂批量合並,是只執行了其中某一次的setState
嗎?執行的是第一次還是最后一次?其實看個例子就懂了:
class Echo extends React.Component {
state = {
a:false,
b:false
}
componentDidUpdate() {
console.log(this.state);
}
handleOnClick = () => {
this.setState({
a:true
}, () => {
console.log(this.state)
});
this.setState({
b: true
}, () => {
console.log(this.state)
});
this.setState({
a: false
}, () => {
console.log(this.state)
});
}
render() {
return (
<>
<button onClick={this.handleOnClick}>click me</button>
</>
)
}
}

事實證明,setState
在批量合並過程中還是會是執行每個setState
,但在updater
是對象的情況下,setState
對於相同key
的操作始終以最后一次修改為准:
// 執行了三次了加1,但最終其實只會加一次
this.setState({num:this.state.num + 1});
this.setState({num:this.state.num + 1});
this.setState({num:this.state.num + 1});
比如上述代碼執行了三次+1操作,等待渲染結束后,我們會發現結果num
其實只加了一個1,它等同於:
const num = this.state.num;
this.setState({num:num + 1});
this.setState({num:num + 1});
this.setState({num:num + 1});
這里的const num = this.state.num;
就相當於是一個快照,setState
確實執行了三次,只是設置的一直都是相同的值,導致最終this.state
的值確確實實是以最后一次為准。
貳 ❀ 叄 什么情況下setState是同步?
其實要回到這個問題,我們只需要知道什么情況下setState
是異步,那么反過來的情況自然就都是同步了。一般來說,react
在事件處理函數內部的 setState
都是異步的,比如合成事件onClick
,onBlur
,其次react
提供的生命周期鈎子函數中也是異步。
那么是不是說只要setState
不在合成事件內調用,我們就能實現同步更新了呢?來看個例子:
class Echo extends React.Component {
state = {
num:1
}
componentDidUpdate() {
console.log(this.state.num);//2 3 4
}
handleOnClick = () => {
setTimeout(()=>{
this.setState({num:this.state.num+1});
this.setState({num:this.state.num+1});
this.setState({num:this.state.num+1});
console.log(this.state.num);//4
})
}
render() {
console.log('我在render了');// 執行3次
return (
<>
<button onClick={this.handleOnClick}>click me</button>
</>
)
}
}

事實上,超出了react
能力范疇之外的上下文,比如原生事件,定時器回調等等,在這里面進行setState
操作都會同步更新state
。比如在上述例子中,我們實現了在setState
后獲取到同步更新的this.state
,但遺憾的是,react
此時並不能做到批量合並操作,導致render
執行了三次。
貳 ❀ 肆 為什么一定要設計成異步,同步批量處理不行嗎?
其實在眾多react
使用者中一直有一個這樣的疑問,雖然我們知道異步本質目的是為了異步累積處理setState
,達到減少render
提升性能的目的。那么問題來了,異步能做到批量處理,同步難道就不行嗎?我們讓state
同步更新,只要在最終render
時做好把控,不是一樣能達到這樣的效果,而且從代碼可讀上來說,同步更利於狀態維護。對此,官方給出了合理解釋,大致分為三點:
-
保證內部的一致性
即便
setState
能做到同步,react
對於props
的更新依舊是異步,這是因為對於一個子組件而言,它只有等到父組件重新渲染了,它才知道最新的props
是多少,所以讓setState
異步的另一個原因是為了讓state,props,refs
更新的行為與表現保持一致。我們假設有下面這段代碼,它是同步執行:console.log(this.state.value);//0 this.setState({value:this.state.value+1}); console.log(this.state.value);//1 this.setState({value:this.state.value+1}); console.log(this.state.value);//2
但現在我們有個場景,這個狀態需要被多個兄弟組件使用,因此我們需要將其狀態提升到父組件,以便於給多個兄弟組件共享:
console.log(this.props.value);//0 this.props.onIncrement(); console.log(this.props.value);//0 this.props.onIncrement(); console.log(this.props.value);//0
很遺憾上述代碼並不能按照我們預期的執行,因為在同步模型中,
this.state
會立刻更新,但是this.props
並不會,而且在沒有重新渲染父組件的情況下,我們沒辦法立刻更新this.props
,那要假設要做到每執行一次onIncrement
能讓兄弟組件都拿到最新的props
,唯一的辦法就是立刻重新渲染父組件,而這種場景下,已經與我們最初的批量合並處理減少重復渲染相違背了。而為了解決這個問題,
react
將this.props
與this.state
更新設計為異步,這也讓狀態提升時對於狀態的管理更合理與更安全。

-
性能優化
如果
setState
是同步的話,那么對於狀態的改變一定會按照setState
調用順序來執行並改變,但事實上react
會根據setState
不同的調用源,為這些setState
分配不同的優先級,調用源包含事件處理,網絡請求,動畫等等。官方給了一個這樣的例子,比如我們在一個聊天窗口聊天,輸入的信息變化會觸發
setState
,而此時我們搜到了一條新消息,新消息也會觸發setState
,那么這里更好的做法是延遲新消息的setState
的執行,降低其優先級,這樣就能避免輸入過程中因為新消息觸發的渲染,導致輸入過程中抖動以及延遲。如果給某些更新分配更低的優先級,那么就可以把它們拆分成幾毫秒的渲染塊,這樣用戶也不會察覺到。 -
異步創造更多可能性
異步除了性能優化之外,異步也為未來的
react
升級埋下更多可能性。比如我們有個需要,需要從頁面A導航到頁面B,那么這時候你可能需要做一個加載動畫,等待B頁面渲染。但如果導航切換特別快,閃爍一下加載動畫又會降低用戶體驗。而站在異步的基礎上,當我們調用
setState
去渲染一個新頁面,因為異步的緣故,react
可以在后台渲染這個新頁面,而且不去阻塞舊頁面的交互,假設等待時間過長,我們還是可以展示loading
,但如果等待耗時非常短暫,setState
可以因為異步批量合並的緣故減少渲染,不會讓頁面頻繁閃動,從而提升用戶體驗。
對於問題的原回答,可閱讀此issues:RFClarification: why is setState
asynchronous?那么到這里,我們站在react
官方的角度解釋了為什么react
中的setState
是異步而不能是同步。
叄 ❀ 總
我們花了較大的篇幅解釋了好幾個setState
相關非常意思的問題,但其實我們還剩余一個問題沒解釋,那就是像合成事件中的setState
會異步執行批量合並操作,而像原生定時器中的setState
卻不會如此。那么react
如何區分這兩者情況,或者說react
在合成事件的底層到底做了什么?
考慮到篇幅的問題,這個問題我打算放在與setState
異步緊急相連的合成事件篇章去解釋,也便於大家對於本篇知識點的快捷梳理與消化。請回到文章開頭再次面對最初的那幾個答案,那么現在你心中是否有了自己的答案?
文章最后附上非常經典的setState
點三次的問題,代碼如下:
class Echo extends React.Component{
state = {
count: 0
}
// count +1
increment = () => {
console.log('increment setState前的count', this.state.count)
this.setState({
count: this.state.count + 1
});
console.log('increment setState后的count', this.state.count)
}
// count +1 三次
triple = () => {
console.log('triple setState前的count', this.state.count)
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
console.log('triple setState后的count', this.state.count)
}
// count - 1
reduce = () => {
setTimeout(() => {
console.log('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1
});
console.log('reduce setState后的count', this.state.count)
}, 0);
}
render(){
return <div>
<button onClick={this.increment}> +1 </button>
<button onClick={this.triple}> +1 三次 </button>
<button onClick={this.reduce}> -1 </button>
</div>
}
}
大家可以自行思考,如果此時你已經能輕松回答,那么你對於setState
同步異步問題已經有了一個清晰的認知了。剩余的問題,我們在下一篇合成事件再詳細闡述,那么到這里本文結束。