setState可能是非同步的

莫珂發表於2018-04-02

提到React相信setState的這個函式你應該不會陌生,這是我們唯一可以修改state並讓觸發元件變化的方法。但是你真的會用嗎?

一個簡單的例子

import React, { Component } from 'react';

class Counter extends Component{
    state = {count : 0} 
    
    incrementCount = () => {
        this.setState({count : this.state.count + 1}) 
        this.setState({count : this.state.count + 1})
    }
    render(){
        return <div>
                <button onClick={this.incrementCount}>Increment</button>
                <div>{this.state.count}</div>
            </div>
    }
}

export default Counter;
複製程式碼

如果你知道我這個例子在說什麼,那麼可以點選右上角關閉這篇文章了,否則請看下圖演示:

Bad Counter

可以看到當我們點選了increment按鈕後,數值只增加了1而不是2。這就是我們今天要討論的問題了。

如何解決這個問題?

實際上我們經常使用的setState方法其實還有第二個引數,一個可選的回撥函式,具體定義如下:

setState(updater[, callback])
複製程式碼

一旦setState完成並且元件重繪之後,這個回撥函式將會被呼叫。所以當我們使用回撥函式這種非同步的呼叫方式的時候就可以避免之前的錯誤。如果不好理解的話可以想象setState是一個網路請求,使用回撥函式才能在正確的時間拿到正確的值。

我們只需要簡單的修改incrementCount方法如下:

incrementCount = () => {
    // this.setState({count : this.state.count + 1}) 
    // this.setState({count : this.state.count + 1})
    this.setState(prevState => {
        return {count: prevState.count + 1}
    });
    this.setState(prevState => {
        return {count: prevState.count + 1}
    });
}
複製程式碼

為什麼會這樣?

其實當你再去查閱React官網這篇文章時候,你不難發現有提到下面一段話:

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall

從這句話中我們看到setState()方法有可能不是立刻更新元件的,他可能會合並一些更新或者推遲一些更新。我們下面就來簡單解釋一下(因為涉及到一些原始碼的實現,對於初學者不太好理解這裡就不講的太複雜,大概看一下就好了

因為在setState的函式實現中,會有一個isBatchingUpdates(預設是false)來判斷是直接更新還是回頭再說。React在呼叫事務處理函式(生命週期函式)之前會呼叫batchedUpdates函式把isBatchingUpdates設定為true,這樣在事件處理過程中就不會同步更新了,而是合併一些setState操作。在我們一開始的例子中兩次加1的操作其實是被React做了一個淺拷貝合併在一起生成新的值,其實相當於

Object.assign(
    previousState,
    {count : state.count + 1},
    {count : state.count + 1}
)
複製程式碼

這樣後來的這次加1就把前面的這次給覆蓋了,所以你就只能看到加1了。

什麼時候需要使用回撥的方式?

當你本次需要setState設定的值是依賴於props或者state計算得到的時候就需要使用回撥的方式。

this.setState((prevState, props) => {
  return {
      //這裡設定屬性
  };
});
複製程式碼

相關文章