setState 的更新是同步仍是異步,一直是人們津津樂道的話題。不過,實際上若是咱們須要用到更新後的狀態值,並不須要強依賴其同步/異步更新機制。在類組件中,咱們能夠經過this.setState
的第二參數、componentDidMount
、componentDidUpdate
等手段來取得更新後的值;而在函數式組件中,則能夠經過useEffect
來獲取更新後的狀態。因此這個問題,其實有點無聊。react
不過,既然你們都這麼樂於討論,今天咱們就係統地梳理一下這個問題,主要分爲兩方面來講:shell
class-component
)的更新機制function-component
)的更新機制在類組件中,這個問題的答案是多樣的,首先拋第一個結論:npm
legacy
模式中,更新可能爲同步,也可能爲異步;concurrent
模式中,必定是異步。經過ReactDOM.render(<App />, rootNode)
方式建立應用,則爲 legacy 模式,這也是create-react-app
目前採用的默認模式;bash
經過ReactDOM.unstable_createRoot(rootNode).render(<App />)
方式建立的應用,則爲concurrent模式
,這個模式目前只是一個實驗階段的產物,還不成熟。app
是的,這不是玄學,咱們來先拋出結論,再來逐步解釋它。dom
this.setState
時,爲異步更新;this.setState
,則爲同步更新;實驗代碼以下:異步
class StateDemo extends React.Component { constructor(props) { super(props) this.state = { count: 0 } } render() { return <div> <p>{this.state.count}</p> <button onClick={this.increase}>累加</button> </div> } increase = () => { this.setState({ count: this.state.count + 1 }) // 異步的,拿不到最新值 console.log('count', this.state.count) // setTimeout 中 setState 是同步的 setTimeout(() => { this.setState({ count: this.state.count + 1 }) // 同步的,能夠拿到 console.log('count in setTimeout', this.state.count) }, 0) } bodyClickHandler = () => { this.setState({ count: this.state.count + 1 }) // 能夠取到最新值 console.log('count in body event', this.state.count) } componentDidMount() { // 本身定義的 DOM 事件,setState 是同步的 document.body.addEventListener('click', this.bodyClickHandler) } componentWillUnmount() { // 及時銷燬自定義 DOM 事件 document.body.removeEventListener('click', this.bodyClickHandler) } }
要解答上述現象,就必須瞭解 setState 的主流程,以及 react 中的 batchUpdate 機制。函數
首先咱們來看看 setState 的主流程:this
this.setState(newState)
;newState
會存入 pending 隊列;batchUpdate
;batchUpdate
,則將組件先保存在所謂的髒組件dirtyComponents
中;若是不是batchUpdate
,那麼就遍歷全部的髒組件,並更新它們。由此咱們能夠斷定:所謂的異步更新,都命中了batchUpdate
,先保存在髒組件中就完事;而同步更新,老是會去更新全部的髒組件。code
很是有意思,看來是否命中batchUpdate
是關鍵。問題也隨之而來了,爲啥直接調用就能命中batchUpdate
,而放在異步回調裏或者自定義 DOM 事件中就命中不了呢?
這就涉及到一個頗有意思的知識點:react 中函數的調用模式。對於剛剛的 increase 函數,還有一些咱們看不到的東西,如今咱們經過魔法讓其顯現出來:
increase = () => { // 開始:默認處於bashUpdate // isBatchingUpdates = true this.setState({ count: this.state.count + 1 }) console.log('count', this.state.count) // 結束 // isBatchingUpdates = false }
increase = () => { // 開始:默認處於bashUpdate // isBatchingUpdates = true setTimeout(() => { // 此時isBatchingUpdates已經設置爲了false this.setState({ count: this.state.count + 1 }) console.log('count in setTimeout', this.state.count) }, 0) // 結束 // isBatchingUpdates = false }
當 react 執行咱們所書寫的函數時,會默認在首位設置isBatchingUpdates
變量。看到其中的差別了嗎?當 setTimeout 執行其回調時,isBatchingUpdates
早已經在同步代碼的末尾被置爲false
了,因此沒命中batchUpdate
。
那自定義 DOM 事件又是怎麼回事?代碼依然以下:
componentDidMount() { // 開始:默認處於bashUpdate // isBatchingUpdates = true document.body.addEventListener("click", () => { // 在回調函數裏面,當點擊事件觸發的時候,isBatchingUpdates早就已經設爲false了 this.setState({ count: this.state.count + 1, }); console.log("count in body event", this.state.count); // 能夠取到最新值。 }); // 結束 // isBatchingUpdates = false }
咱們能夠看到,當componentDidMount
跑完時,isBatchingUpdates
已經設置爲false
了,而點擊事件後來觸發,並調用回調函數時,取得的isBatchingUpdates
固然也是false
,不會命中batchUpdate
機制。
總結:
this.setState
是同步仍是異步,關鍵就是看可否命中batchUpdate
機制isBatchingUpdates
是true
仍是false
batchUpdate
的場景包括:生命週期和其調用函數、React中註冊的事件和其調用函數。總之,是React能夠「管理」的入口,關鍵是「入口」。這裏要注意一點:React去加isBatchingUpdate的行爲不是針對「函數」,而是針對「入口」。好比setTimeout、setInterval、自定義DOM事件的回調等,這些都是React「管不到」的入口,因此不會去其首尾設置isBatchingUpdates變量。
由於這個東西只在實驗階段,因此要開啓 concurrent 模式,一樣須要將 react 升級爲實驗版本,安裝以下依賴:
npm install react@experimental react-dom@experimental
其餘代碼不用變,只更改 index 文件以下:
- ReactDOM.render(<App />, document.getElementById('root')); + ReactDOM.unstable_createRoot(document.getElementById('root')).render(<App />);
則能夠發現:其更新都是異步的,在任何狀況下都是如此。
在函數式組件中,咱們會這樣定義狀態:
const [count, setCount] = useState(0)
這時候,咱們發現當咱們不管在同步函數仍是在異步回調中調用 setCount 時,打印出來的 count 都是舊值,這時候咱們會說:setCount 是異步的。
const [count, setCount] = useState(0); // 直接調用 const handleStrightUpdate = () => { setCount(1); console.log(count); // 0 }; // 放在setTimeout回調中 const handleSetTimeoutUpdate = () => { setTimeout(() => { setCount(1); console.log(count); // 0 }); };
setCount 是異步的,這確實沒錯,可是產生上述現象的緣由不僅是異步更新這麼簡單。緣由主要有如下兩點:
1,調用 setCount 時,會作合併處理,異步更新該函數式組件對應的 hooks 鏈表裏面的值,而後觸發重渲染(re-renders
),從這個角度上來講,setCount
確實是一個異步操做;
2,函數式的capture-value
特性決定了console.log(count)
語句打印的始終是一個只存在於當前幀的常量,因此就算不管 setCount 是否是同步的,這裏都會打印出舊值。