1. 程式人生 > >react之setState解析

react之setState解析

在react開發過程中,state是元件一個重要的屬性,對state的管理也尤為重要,這裡記錄一下踩坑經歷

修改state正確方式

對於修改元件state正確的方式如下:

// 錯誤
this.state.title = 'React';

// 正確
this.setState({title: 'React'});

有兩個比較重要的點,一是對state的修改是非同步,二是對多個相鄰的state的修改可能會合併到一起一次執行。程式碼舉例:

非同步執行

class Clock extends React.Component {
  constructor(props) {
    super
(props); this.state = { num: 1 }; } handleClick() { this.setState((prevState, props) => ({ num: prevState.num + 1 })); console.info(this.state.num); } render() { return ( <div> <h1 onClick={this.handleClick.bind(this)}>Hello, world!</h1> <h2>It is
{this.state.num}.</h2> </div> )
; } } ReactDOM.render(<Clock />, document.getElementById("root"));

結果在ui顯示每一次加1之後的結果,而console列印的總是上一次ui的值,也就可以確定state並沒有立即執行,而是非同步去執行的。我們可以通過新增一個回撥函式到第二個引數,讓state更新完成之後來執行該回調,程式碼如下:

handleClick() {
    this.setState({
      num : this.state
.num + 1 }); }

此時ui上顯示的數值與console列印的會是一致。另一種情況,如果對state的修改依賴state和prop,那麼由於非同步的原因就會產生很多異常情況,我們可以可以傳入包含兩個引數的函式到第一個引數來解決,程式碼如下:

handleClick() {
    this.setState((prevState, props) => ({
      num: prevState.num + 1
    }),() => console.info(this.state.num));

  }

第一個引數prevState表示前一次的state,後一個引數表示當前狀態元件的prop值,顯示的讓該操作同步執行

批量單次執行

對於多次相鄰的state修改操作的執行會被合併在一起執行,程式碼如下:

handleClick() {
    this.setState({
      num : this.state.num + 1
    },() => console.info(this.state.num));
    this.setState({
      num : this.state.num + 1
    },() => console.info(this.state.num));
    this.setState({
      num : this.state.num + 1
    },() => console.info(this.state.num));
  }

最終數值每次只會加1,我們可以看作是以下程式碼的執行:

Object.assign(
  previousState,
  {num : this.state.num + 1},
  {num : this.state.num + 1}
)

因此,被合併之後最終只會保留一個更新。解決方法和上述解決非同步的方式類似:

handleClick() {
    this.setState((prevState,props) => ({
      num : prevState.num + 1
    }),() => console.info(this.state.num));
    this.setState((prevState,props) => ({
      num : prevState.num + 1
    }),() => console.info(this.state.num));
    this.setState((prevState,props) => ({
      num : prevState.num + 1
    }),() => console.info(this.state.num));
  }

結果是我們期望的每次加3,但是需要注意的是,console列印的只會是每次加3之後的數值,並不會是每次加1的結果

State與Immutable

React官方建議把State當作是不可變物件,一方面是如果直接修改this.state,元件並不會重新render;另一方面State中包含的所有狀態都應該是不可變物件。當State中的某個狀態發生變化,我們應該重新建立這個狀態物件,而不是直接修改原來的狀態。那麼,當狀態發生變化時,如何建立新的狀態呢?根據狀態的型別,可以分成三種情況:

  1. 狀態的型別是不可變型別(數字,字串,布林值,null, undefined)
    這種情況最簡單,因為狀態是不可變型別,直接給要修改的狀態賦一個新值即可。如要修改count(數字型別)、title(字串型別)、success(布林型別)三個狀態:

    this.setState({
    count: 1,
    title: 'Redux',
    success: true
    })
  2. 狀態的型別是陣列
    如有一個數組型別的狀態books,當向books中增加一本書時,使用陣列的concat方法或ES6的陣列擴充套件語法(spread syntax):

    // 方法一:將state先賦值給另外的變數,然後使用concat建立新陣列
    var books = this.state.books; 
    this.setState({
    books: books.concat(['React Guide']);
    })
    // 方法二:使用preState、concat建立新陣列
    this.setState(preState => ({
    books: preState.books.concat(['React Guide']);
    }))
    // 方法三:ES6 spread syntax
    this.setState(preState => ({
    books: [...preState.books, 'React Guide'];
    }))

    當從books中擷取部分元素作為新狀態時,使用陣列的slice方法:

    // 方法一:將state先賦值給另外的變數,然後使用slice建立新陣列
    var books = this.state.books; 
    this.setState({
    books: books.slice(1,3);
    })
    // 方法二:使用preState、slice建立新陣列
    this.setState(preState => ({
    books: preState.books.slice(1,3);
    }))

    注意不要使用push、pop、shift、unshift、splice等方法修改陣列型別的狀態,因為這些方法都是在原陣列的基礎上修改,而concat、slice、filter會返回一個新的陣列。

  3. 狀態的型別是普通物件(不包含字串、陣列)
    3.1 使用ES6 的Object.assgin方法

    // 方法一:將state先賦值給另外的變數,然後使用Object.assign建立新物件
    var owner = this.state.owner;
    this.setState({
    owner: Object.assign({}, owner, {name: 'Jason'});
    })
    // 方法二:使用preState、Object.assign建立新物件
    this.setState(preState => ({
    owner: Object.assign({}, preState.owner, {name: 'Jason'});
    }))

    3.2 使用物件擴充套件語法

    // 方法一:將state先賦值給另外的變數,然後使用物件擴充套件語法建立新物件
    var owner = this.state.owner;
    this.setState({
    owner: {...owner, name: 'Jason'};
    })
    // 方法二:使用preState、物件擴充套件語法建立新物件
    this.setState(preState => ({
    owner: {...preState.owner, name: 'Jason'};
    }))