說說React組件的State


說說React組件的State

React的核心思想是組件化的思想,應用由組件搭建而成, 而組件中最重要的概念是State(狀態)。

正確定義State

React把組件看成一個狀態機。通過與用戶的交互,實現不同狀態,然后渲染UI,讓用戶界面和數據保持一致。組件的任何UI改變,都可以從State的變化中反映出來;State中的所有狀態都用於反映UI的變化,不應有多余狀態。

那么什么樣的變量應該做為組件的State呢:

  1. 可以通過props從父組件中獲取的變量不應該做為組件State。
  2. 這個變量如果在組件的整個生命周期中都保持不變就不應該作為組件State。
  3. 通過其他狀態(State)或者屬性(Props)計算得到的變量不應該作為組件State。
  4. 沒有在組件的render方法中使用的變量不用於UI的渲染,那么這個變量不應該作為組件的State 。這種情況下,這個變量更適合定義為組件的一個普通屬性。

React中的immutability

React官方建議把State當做是不可變對象,State中包含的所有狀態都應該是不可變對象,當State中的某個狀態發生變化,我們應該重新創建這個狀態對象,而不是直接修改原來的狀態。State根據狀態類型可以分為三種。

  1. 數字,字符串,布爾值,null,undefined這五種不可變類型。

因為其本身就是不可變的,如果要修改狀態的話,直接賦新值就可以,例如:

this.setState({
  num: 1,
  string: 'hello',
  ready: true
});

2、數組類型

js中數組類型為可變類型。假如有一個state是數組類型,例如students。修改students的狀態應該保證不會修改原來的狀態,
例如新增一個數組元素,應使用數組的concat方法或ES6的數組擴展語法。

  let students = this.state.students;
  this.setState({
    students: students.concat(['xiaoming'])
  });
  
  //或者
  this.setState(preState => ({
    students: [ ...preState.books, 'xiaogang']
  });

從數組中截取部分作為新狀態時,應使用slice方法;當從數組中過濾部分元素后,作為新狀態時,使用filter方法。不應該使用push、pop、shift、unshift、splice等方法修改數組類型的狀態,因為這些方法都是在原數組的基礎上修改的。應當使用不會修改原數組而返回一個新數組的方法,例如concat、slice、filter等。
3. 普通對象

對象也是可變類型,修改對象類型的狀態時,應該保證不會修改原來的狀態。可以使用ES6的Object.assign方法或者對象擴展語法。

//Object.assign方法
this.setState(preState => ({
  school: Object.assign({}, preState.school, {classNum: 10})
}));

//對象擴展語法
let school = this.state.school;
this.setState({
  school: { ...school, { classNum: 10 } }
})

不同方式創建的組件中的State

  1. 無狀態組件(Stateless Functional Component)

這種組件自身沒有狀態,不需要管理state狀態,所有數據都是從props傳入。

const Teacher = ({
  name,
  age
}) => {
  return (
    <div>Teacher {name} is {age} years old.</div>
  )
}

相同的輸入(props)必然有相同的輸出,因此這種組件可以寫成無副作用的純函數,且適合函數式編程(函數的compose,curring等組合方式)
2. 純組件(PureComponent)

我們知道,當組件的props或者state發生變化的時候React會對組件當前的Props和State分別與nextProps和nextState進行比較,當發現變化時,就會對當前組件以及子組件進行重新渲染,否則就不渲染。有時候我們會使用shouldUpdateComponent來避免不必要的渲染。當然有時候這種簡單的判斷,顯得有些多余和樣板化,於是react就提供了PureComponent來自動幫我們完成這件事,簡化了我們的代碼,提高了性能。例如:

class CounterButton extends React.pureComponent {
  constructor(props) {
    super(props)
    this.state = { count: 1 };
  }
  render() {
    return (
      <button 
        color={this.props.color}
        onClick={() => this.setState(state = > ({count: state.count + 1}))}
      >
       Count: {this.state.coount}
      </button>
    )
  }
}

在上例中,雖然沒有添加shouldUpdateComponent代碼,但是react自動完成了props和state的比較,當props和state沒有發生變化時不會對組件重新渲染。但是PureComponent的自動為我們添加的shouldComponentUpate函數,只是對props和state進行淺比較(shadowcomparison),當props或者state本身是嵌套對象或數組等時,淺比較並不能得到預期的結果,這會導致實際的props和state發生了變化,但組件卻沒有更新的問題。

淺比較:比較 Object.keys(state | props) 的長度是否一致,每一個 key 是否兩者都有,並且是否是一個引用,也就是只比較了第一層的值,確實很淺,所以深層的嵌套數據是對比不出來的。

例如

class Ul extends PureComponent {
  constructor(props) {
    super(props)
    this.state = { 
      items: [1, 2, 3] 
    };
  }
  handleClick = () => {
    let { items } = this.state;
    items.push(4);
    this.setState({ items });
  }
  render() {
    return (
    <div>
      <ul>
        {this.state.items.map(i => <li key={i}>{i}</li>)}
      </ul>
      <button onClick={this.handleClick}>add</button>
    </div>)
  }
}

會發現,無論怎么點 add 按鈕, li 都不會變多,因為 pop方法是在原數組上進行的修改,items的preState與nextState 用的是一個引用, shallowEqual 的結果為 true 。改正:

handleClick = () => {
  let { items } = this.state;
  this.setState({ items: items.concat([4]) });
}

這樣每次改變都會產生一個新的數組,items的preState與nextState 用的是不同引用, shallowEqual 的結果為 false,也就可以 render 了。
在PureComponent中要避免可變對象作為props和state,你可以考慮使用Immutable.js來創建不可變對象,Immutable Data就是一旦創建,就不能再被更改
的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象。從而避免出現props和state發生改變而組件沒有重新渲染的問題。
3. Component

與PureComponent不同的是,Component需要開發者顯示定義shouldUpdateComponent且定制性更強。對於一些無論怎么修改都不應該讓組件重新渲染的props就不必在shouldUpdateComponent中進行比較。同PureComponent一樣Component中的state也應該是不可變對象。

使用Object.assign或者concat等方法避免修改原來的對象或數組是通過將屬性/元素從一個對象/數組復制到另一個來工作,這種拷貝方式只是淺拷貝。Object.assign()方法實行的就是淺拷貝,而不是深拷貝。也就是說源對象某個屬性的值是對象,那么目標對象拷貝得到的是這個對象的引用。例如有school狀態:

this.state = {
  school: {
    classOne: {
      ...
    },
    classTwo: {
      ...
    }
  }
}

通過Object.assign修改狀態

let classOne = {
  teacher: [...],
  students: [...],
};
this.setState(preState => ({
  school: Object.assign({}, preState.school, {classOne: classOne})
}));

上面代碼中Object.assign拷貝的只是classOne對象的引用,任何對classOne的改變都會反映到React的State中。深拷貝並不是簡單的復制引用,而是在堆中重新分配內存,並且把源對象實例的所有屬性都新建復制,以保證復制的對象的引用不指向任何原有對象上或其屬性內的任何對象,復制后的對象與原來的對象是完全隔離的(關於深拷貝可參考這篇文章)。
深拷貝通常需要把整個對象遞歸的復制一份,十分影響性能。對於大型對象/數組來說,操作比較慢。當應用復雜后,props和state也變得復雜和龐大,通過淺拷貝和深拷貝就會影響性能和造成內存的浪費。且對象和數組默認是可變的,沒有什么可以確保state是不可變對象,你必須時刻記住要使用這些方法。

使用immutable.js可以很好的解決這些問題。Immutable.js 的基本原則是對於不變的對象返回相同的引用,而對於變化的對象,返回新的引用。同時為了避免 deepCopy 把所有節點都復制一遍帶來的性能損耗,Immutable 使用了 Structural Sharing(結構共享),即如果對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。(關於immutab.js可以看這篇文章或者immutable.js)

總結

正確的定義state不僅便於狀態的管理與調試,而且在復雜應用中保持state的簡潔,在組件更新時能減少比較次數,提高性能。保證state的不可變性不僅保證數據更容易追蹤、推導,而且能避免組件更新時shouldComponent出現狀態比較錯誤。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM