setState同步非同步場景
React
通過this.state
來訪問state
,通過this.setState()
方法來更新state
,當this.setState()
方法被呼叫的時候,React
會重新呼叫render
方法來重新渲染UI
。相比較於在使用Hooks
完成元件下所需要的心智負擔,setState
就是在使用class
完成元件下所需要的心智負擔,當然所謂的心智負擔也許叫做所必須的基礎知識更加合適一些。
描述
setState
只在合成事件和生命週期鉤子函式中是非同步的,而在原生事件中都是同步的,簡單實現一個React Class TS
例子。
import React from "react";
import "./styles.css";
interface Props{}
interface State{
count1: number;
count2: number;
count3: number;
}
export default class App extends React.Component<Props, State>{
constructor(props: Props){
super(props);
this.state = {
count1: 0,
count2: 0,
count3: 0
}
}
incrementAsync = () => {
console.log("incrementAsync before setState", this.state.count1);
this.setState({
count1: this.state.count1 + 1
});
console.log("incrementAsync after setState", this.state.count1);
}
incrementSync = () => {
setTimeout(() => {
console.log("incrementSync before setState", this.state.count2);
this.setState({
count2: this.state.count2 + 1
});
console.log("incrementSync after setState", this.state.count2);
},0);
}
incrementAsyncFn = () => {
console.log("incrementAsyncFn before setState", this.state.count3);
this.setState({
count3: this.state.count3 + 1
},
() => {
console.log("incrementAsyncFn after.1 setState", this.state.count3);
}
);
this.setState(state => {
console.log("incrementAsyncFn after.2 setState", state.count3);
return {count3: state.count3}
});
}
render(){
return <div>
<button onClick={this.incrementAsync}>非同步</button>
<button onClick={this.incrementSync}>同步</button>
<button onClick={this.incrementAsyncFn}>非同步函式引數</button>
</div>
}
}
以此點選三個按鈕的話,可以得到以下輸出。
incrementAsync before setState 0
incrementAsync after setState 0
incrementSync before setState 0
incrementSync after setState 1
incrementAsyncFn before setState 0
incrementAsyncFn after.2 setState 1
incrementAsyncFn after.1 setState 1
首先看incrementAsync
的結果,在這裡我們可以看出,在合成事件呼叫setState
之後,this.state
是無法立即得到最新的值。
對於incrementSync
的結果,在非合成事件的呼叫時,this.state
是可以立即得到最新的值的,例如使用addEventListener
、setTimeout
、setInterval
等。
對於incrementAsyncFn
的兩個結果,首先來說after.2
的結果,對於this.state
也是可以得到最新的值的,如果你需要基於當前的state
來計算出新的值,那麼就可以通過傳遞一個函式,而不是一個物件的方式來實現,因為setState
的呼叫是分批的,所以通過傳遞函式可以鏈式地進行更新,當然前提是需要確保它們是一個建立在另一個之上的,也就是說傳遞函式的setState
的值是依賴於上次一的SetState
的,對於after.1
的結果,setState
函式的第二個引數是一個回撥函式,在setState
批量更新完成後可以拿到最新的值,而after.2
也是屬於批量更新需要呼叫的函式,所以after.1
會在after.2
後執行。
原理
React
將其實現為非同步的動機主要是效能的考量,setState
的非同步並不是說內部由非同步程式碼實現,其實本身執行的過程和程式碼都是同步的,只是合成事件和生命週期鉤子函式的呼叫順序在批處理更新之前,導致在合成事件和生命週期鉤子函式中沒法立馬拿到更新後的值,形式了所謂的非同步,實際上是否進行批處理是由其內部的isBatchingUpdates
的值來決定的。
setState
依賴於合成事件,合成事件指的是React
並不是將click
等事件直接繫結在DOM
上面,而是採用事件冒泡的形式冒泡到頂層DOM
上,類似於事件委託,然後React
將事件封裝給正式的函式處理執行和處理。說完了合成事件再回到setState
,setState
的批量更新優化也是建立在合成事件上的,其會將所有的setState
進行批處理,如果對同一個值進行多次 setState
,setState
的批量更新策略會對其進行覆蓋,取最後一次的執行,如果是同時setState
多個不同的值,在更新時也會對其進行合併批量更新,而在原生事件中,值會立即進行更新。
採用批量更新,簡單來說就是為了提升效能,因為不採用批量更新,在每次更新資料都會對元件進行重新渲染,舉個例子,讓我們在一個方法內重複更新一個值。
this.setState({ msg: 1 });
this.setState({ msg: 2 });
this.setState({ msg: 3 });
事實上,我們真正想要的其實只是最後一次更新而已,也就是說前三次更新都是可以省略的,我們只需要等所有狀態都修改好了之後再進行渲染就可以減少一些效能損耗。還有一個例子,如果更改了N
個狀態,其實只需要一次setState
就可以將DOM
更新到最新,如果我們更新多個值。
this.setState({ msg: 1 });
this.setState({ age: 2 });
this.setState({ name: 3 });
此處我們分三次修改了三種狀態,但其實React
只需要渲染一次,在setState
批處理之後會將其合併,並進行一次re-render
就可以將整個元件的DOM
更新到最新,根本不需要關心這個setState
到底是從哪個具體的狀態發出來的。
那麼還有一個問題,首先我們可以認同進行批處理更新對我們的效能是有益的,例如Child
和Parent
都呼叫setState
,我們不需要重新渲染Child
兩次。但是此時我們可能會想到一個問題,為什麼不能如同Vue
一樣,Vue
是在值更新之後觸發setter
然後進行更新,更新的過程同樣也是採用非同步渲染,也會將所有觸發Watcher
的update
進行去重合並再去更新檢視,也就是說Vue
是立即修改了值,而後再去更新檢視的。也就是說,相比較於React
,為什麼不能在同樣做批處理的情況下,立即將setState
更新寫入this.state
而不等待協調結束。
任何一種解決方案都有權衡,對於Vue
來說因為其是通過劫持了資料的setter
過程,在使用的也是直接使用=
直接賦值的,而在賦值之後進行檢視的更新也是一個自然的過程,如果類似於React
一樣在=
之後這個值依然沒有變化,在直覺上是非常不符合常理的,雖然Vue
是通過劫持setter
實現的檢視更新,但是做到如同React
一樣並不是不可能的,這是Vue
採用的解決方案上的權衡,當然這只是可能的一個理由,說是問題的權衡,實際上還是需要對比React
來看,對於React
中要處理的問題,Vue
自然會有自己解決方案的權衡,歸根到底還是框架的設計哲學的問題。對於上面提出的在同樣做批處理的情況下,立即將setState
更新寫入this.state
而不等待協調結束的這個問題,dan
給予了兩個理由,在此簡作總結,完整的英文版本還請看參考中的github issue
。
保證內部資料統一
即使state
是同步更新的,但props
是不會的,在重新渲染父元件之前,無法知道props
,如果同步執行此操作,批處理就會消失。現在React
提供的物件state
、props
、refs
在內部是一致的。這意味著如果只使用這些物件,則可以保證它們引用完全協調的樹,即使它是該樹的舊版本。當僅使用state
時,同步重新整理的模式將起作用。
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
但是,假設需要提升此狀態以在幾個元件之間共享,因此將其移動到父級,也就是說有props
參與到了傳值,那麼同步setState
模式就會有問題,此時將state
提升到了父元件,利用props
將值傳導子元件。
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
,這意味著我們將不得不放棄批處理的策略。還有更微妙的情況說明這如何破壞一致性的,例如這種方案正在混合來自props
尚未重新整理和state
建議立即重新整理的資料以建立新狀態。在React
中,this.state
和this.props
都只在協調和重新整理之後更新,所以你會在refactoring
之前和之後看到0
被列印出來。這使得提升狀態安全。在某些情況下這可能會帶來不便,特別是對於來自更多OO
背景的人來說,他們只想多次改變狀態,而不是考慮如何在一個地方表示完整的狀態更新,我可以理解這一點,儘管我確實認為從除錯的角度來看,保持狀態更新的集中更加清晰。總而言之,React
模型並不總是會產生最簡潔的程式碼,但它在內部是一致的,並確保提升狀態是安全的。
啟用併發更新
從概念上講React
的行為就好像每個元件都有一個更新佇列,我們在這裡討論是否同步重新整理state
有一個前提那就是我們預設更新節點是遵循特定的順序的,但是按預設順序更新元件在以後的react
中可能就變了。對於現在我們一直在談論的非同步渲染,我承認我們在傳達這意味著什麼方面做得不是很好,但這就是研發的本質:你追求一個在概念上看起來很有前途的想法,但只有在花了足夠的時間之後才能真正理解它的含義。
對於這個理由,是React
發展的一個方向。我們一直在解釋非同步渲染的一種方式是React
可以根據setState()
呼叫的來源分配不同的優先順序:事件處理程式、網路響應、動畫等。例如你現在正在打字,那麼TextBox
元件需要實時的重新整理,但是當你在輸入的時候,來了一個資訊,這個時候可能讓資訊的渲染延遲到某個閾值,而不是因為阻塞執行緒而讓輸入卡頓。如果我們讓某些更新具有較低優先順序,我們可以將它們的渲染分成幾毫秒的小塊,這樣使用者就不會注意到它們。非同步rendering
不僅僅是效能上的優化,我們認為這是React
元件模型可以做什麼的根本性轉變。例如,考慮從一個螢幕導航到另一個螢幕的情況,通常會在渲染新螢幕時顯示一個導航器,但是如果導航速度足夠快,閃爍並立即隱藏導航器會導致使用者體驗下降,更糟糕的是如果有多個級別的元件具有不同的非同步依賴項例如資料、程式碼、影像等,您最終會得到一連串短暫閃爍的導航器。由於所有的DOM
重排,這既在視覺上令人不快,又使您的應用程式在實踐中變慢。如果當您執行一個簡單的setState()
來呈現不同的檢視時,我們可以開始在後臺呈現更新後的檢視。如果您自己不編寫任何協調程式碼,您可以選擇在更新時間超過某個閾值時顯示導航器,否則當整個新子樹的非同步依賴項是時讓React
執行無縫轉換使滿意。請注意,這只是可能的,因為this.state
不會立即重新整理,如果它被立即重新整理,我們將無法開始在後臺渲染檢視的新版本,而舊版本仍然可見且可互動,他們獨立的狀態更新會發生衝突。
每日一題
https://github.com/WindrunnerMax/EveryDay
參考
https://www.jianshu.com/p/cc12e9a8052c
https://juejin.cn/post/6844903636749778958
https://zh-hans.reactjs.org/docs/faq-state.html
https://blog.csdn.net/zz_jesse/article/details/118282921
https://blog.csdn.net/weixin_44874595/article/details/104270057
https://github.com/facebook/react/issues/11527#issuecomment-360199710