說說React元件的State

發表於2018-07-06

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這五種不可變型別。因為其本身就是不可變的,如果要修改狀態的話,直接賦新值就可以,例如:
  2. 陣列型別js中陣列型別為可變型別。假如有一個state是陣列型別,例如students。修改students的狀態應該保證不會修改原來的狀態,
    例如新增一個陣列元素,應使用陣列的concat方法或ES6的陣列擴充套件語法。

    從陣列中擷取部分作為新狀態時,應使用slice方法;當從陣列中過濾部分元素後,作為新狀態時,使用filter方法。不應該使用push、pop、shift、unshift、splice等方法修改陣列型別的狀態,因為這些方法都是在原陣列的基礎上修改的。應當使用不會修改原陣列而返回一個新陣列的方法,例如concat、slice、filter等。
  3. 普通物件物件也是可變型別,修改物件型別的狀態時,應該保證不會修改原來的狀態。可以使用ES6的Object.assign方法或者物件擴充套件語法。

不同方式建立的元件中的State

  1. 無狀態元件(Stateless Functional Component)相同的輸入(props)必然有相同的輸出,因此這種元件可以寫成無副作用的純函式,且適合函數語言程式設計(函式的compose,curring等組合方式)

    相同的輸入(props)必然有相同的輸出,因此這種元件可以寫成無副作用的純函式,且適合函數語言程式設計(函式的compose,curring等組合方式)
  2. 純元件(PureComponent)我們知道,當元件的props或者state發生變化的時候React會對元件當前的Props和State分別與nextProps和nextState進行比較,當發現變化時,就會對當前元件以及子元件進行重新渲染,否則就不渲染。有時候我們會使用shouldUpdateComponent來避免不必要的渲染。當然有時候這種簡單的判斷,顯得有些多餘和樣板化,於是react就提供了PureComponent來自動幫我們完成這件事,簡化了我們的程式碼,提高了效能。例如:

    在上例中,雖然沒有新增shouldUpdateComponent程式碼,但是react自動完成了props和state的比較,當props和state沒有發生變化時不會對元件重新渲染。但是PureComponent的自動為我們新增的shouldComponentUpate函式,只是對props和state進行淺比較(shadowcomparison),當props或者state本身是巢狀物件或陣列等時,淺比較並不能得到預期的結果,這會導致實際的props和state發生了變化,但元件卻沒有更新的問題。淺比較:比較 Object.keys(state | props) 的長度是否一致,每一個 key 是否兩者都有,並且是否是一個引用,也就是隻比較了第一層的值,確實很淺,所以深層的巢狀資料是對比不出來的。

    例如

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

    這樣每次改變都會產生一個新的陣列,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狀態:

    通過Object.assign修改狀態

    上面程式碼中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出現狀態比較錯誤。

相關文章