天天看點

java setstate,深入剖析setState同步異步機制

關于 setState

setState 的更新是同步還是異步,一直是人們津津樂道的話題。不過,實際上如果我們需要用到更新後的狀态值,并不需要強依賴其同步/異步更新機制。在類元件中,我們可以通過this.setState的第二參數、componentDidMount、componentDidUpdate等手段來取得更新後的值;而在函數式元件中,則可以通過useEffect來擷取更新後的狀态。是以這個問題,其實有點無聊。

不過,既然大家都這麼樂于讨論,今天我們就系統地梳理一下這個問題,主要分為兩方面來說:

類元件(class-component)的更新機制

函數式元件(function-component)的更新機制

類元件中的 this.setState

在類元件中,這個問題的答案是多樣的,首先抛第一個結論:

在legacy模式中,更新可能為同步,也可能為異步;

在concurrent模式中,一定是異步。

問題一、legacy 模式和 concurrent 模式是什麼鬼?

通過ReactDOM.render(, rootNode)方式建立應用,則為 legacy 模式,這也是create-react-app目前采用的預設模式;

通過ReactDOM.unstable_createRoot(rootNode).render()方式建立的應用,則為concurrent模式,這個模式目前隻是一個實驗階段的産物,還不成熟。

legacy 模式下可能同步,也可能異步?

是的,這不是玄學,我們來先抛出結論,再來逐漸解釋它。

當直接調用時this.setState時,為異步更新;

當在異步函數的回調中調用this.setState,則為同步更新;

當放在自定義 DOM 事件的處理函數中時,也是同步更新。

實驗代碼如下:

class StateDemo extends React.Component { constructor(props) { super(props) this.state = { count: 0 } } render() { return

{this.state.count}

累加 } 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.setState(newState);

newState會存入 pending 隊列;

3,判斷是不是batchUpdate;

4,如果是batchUpdate,則将元件先儲存在所謂的髒元件dirtyComponents中;如果不是batchUpdate,那麼就周遊所有的髒元件,并更新它們。

由此我們可以判定:所謂的異步更新,都命中了batchUpdate,先儲存在髒元件中就完事;而同步更新,總是會去更新所有的髒元件。

非常有意思,看來是否命中batchUpdate是關鍵。問題也随之而來了,為啥直接調用就能命中batchUpdate,而放在異步回調裡或者自定義 DOM 事件中就命中不了呢?

這就涉及到一個很有意思的知識點:react 中函數的調用模式。對于剛剛的 increase 函數,還有一些我們看不到的東西,現在我們通過魔法讓其顯現出來:

increase = () => { // 開始:預設處于bashUpdate // isBatchingUpdates = true this.setState({ count: t.........