React 深入系列3:Props 和 State

iKcamp發表於2019-03-04

文:徐超,《React進階之路》作者

授權釋出,轉載請註明作者及出處


React 深入系列3:Props 和 State

React 深入系列,深入講解了React中的重點概念、特性和模式等,旨在幫助大家加深對React的理解,以及在專案中更加靈活地使用React。

React 的核心思想是元件化的思想,而React 元件的定義可以通過下面的公式描述:

UI = Component(props, state)
複製程式碼

元件根據props和state兩個引數,計算得到對應介面的UI。可見,props 和 state 是元件的兩個重要資料來源。

本篇文章不是對props 和state 基本用法的介紹,而是嘗試從更深層次解釋props 和 state,並且歸納使用它們時的注意事項。

Props 和 State 本質

**一句話概括,props 是元件對外的介面,state 是元件對內的介面。**元件內可以引用其他元件,元件之間的引用形成了一個樹狀結構(元件樹),如果下層元件需要使用上層元件的資料或方法,上層元件就可以通過下層元件的props屬性進行傳遞,因此props是元件對外的介面。元件除了使用上層元件傳遞的資料外,自身也可能需要維護管理資料,這就是元件對內的介面state。根據對外介面props 和對內介面state,元件計算出對應介面的UI。

元件的props 和 state都和元件最終渲染出的UI直接相關。兩者的主要區別是:state是可變的,是元件內部維護的一組用於反映元件UI變化的狀態集合;而props是元件的只讀屬性,元件內部不能直接修改props,要想修改props,只能在該元件的上層元件中修改。在元件狀態上移的場景中,父元件正是通過子元件的props,傳遞給子元件其所需要的狀態。

如何定義State

定義一個合適的state,是正確建立元件的第一步。state必須能代表一個元件UI呈現的完整狀態集,即元件對應UI的任何改變,都可以從state的變化中反映出來;同時,state還必須是代表一個元件UI呈現的最小狀態集,即state中的所有狀態都是用於反映元件UI的變化,沒有任何多餘的狀態,也不需要通過其他狀態計算而來的中間狀態。

元件中用到的一個變數是不是應該作為元件state,可以通過下面的4條依據進行判斷:

  1. 這個變數是否是通過props從父元件中獲取?如果是,那麼它不是一個狀態。
  2. 這個變數是否在元件的整個生命週期中都保持不變?如果是,那麼它不是一個狀態。
  3. 這個變數是否可以通過state 或props 中的已有資料計算得到?如果是,那麼它不是一個狀態。
  4. 這個變數是否在元件的render方法中使用?如果不是,那麼它不是一個狀態。這種情況下,這個變數更適合定義為元件的一個普通屬性(除了props 和 state以外的元件屬性 ),例如元件中用到的定時器,就應該直接定義為this.timer,而不是this.state.timer。

請務必牢記,並不是元件中用到的所有變數都是元件的狀態!當存在多個元件共同依賴同一個狀態時,一般的做法是狀態上移,將這個狀態放到這幾個元件的公共父元件中。

如何正確修改State

1.不能直接修改State。

直接修改state,元件並不會重新重發render。例如:

// 錯誤
this.state.title = `React`;
複製程式碼

正確的修改方式是使用setState():

// 正確
this.setState({title: `React`});
複製程式碼
2. State 的更新是非同步的。

呼叫setState,元件的state並不會立即改變,setState只是把要修改的狀態放入一個佇列中,React會優化真正的執行時機,並且React會出於效能原因,可能會將多次setState的狀態修改合併成一次狀態修改。所以不能依賴當前的state,計算下個state。當真正執行狀態修改時,依賴的this.state並不能保證是最新的state,因為React會把多次state的修改合併成一次,這時,this.state還是等於這幾次修改發生前的state。另外需要注意的是,同樣不能依賴當前的props計算下個state,因為props的更新也是非同步的。

舉個例子,對於一個電商類應用,在我們的購物車中,當點選一次購買按鈕,購買的數量就會加1,如果我們連續點選了兩次按鈕,就會連續呼叫兩次this.setState({quantity: this.state.quantity + 1}),在React合併多次修改為一次的情況下,相當於等價執行了如下程式碼:

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

於是乎,後面的操作覆蓋掉了前面的操作,最終購買的數量只增加了1個。

如果你真的有這樣的需求,可以使用另一個接收一個函式作為引數的setState,這個函式有兩個引數,第一個引數是元件的前一個state(本次元件狀態修改成功前的state),第二個引數是元件當前最新的props。如下所示:

// 正確
this.setState((preState, props) => ({
  counter: preState.quantity + 1; 
}))
複製程式碼
3. State 的更新是一個淺合併(Shallow Merge)的過程。

當呼叫setState修改元件狀態時,只需要傳入發生改變的狀態變數,而不是元件完整的state,因為元件state的更新是一個淺合併(Shallow Merge)的過程。例如,一個元件的state為:

this.state = {
  title : `React`,
  content : `React is an wonderful JS library!`
}
複製程式碼

當只需要修改狀態title時,只需要將修改後的title傳給setState

this.setState({title: `Reactjs`});
複製程式碼

React會合並新的title到原來的元件state中,同時保留原有的狀態content,合併後的state為:

{
  title : `Reactjs`,
  content : `React is an wonderful JS library!`
}
複製程式碼

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):

// 方法一:使用preState、concat建立新陣列
this.setState(preState => ({
  books: preState.books.concat([`React Guide`]);
}))

// 方法二:ES6 spread syntax
this.setState(preState => ({
  books: [...preState.books, `React Guide`];
}))
複製程式碼

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

// 使用preState、slice建立新陣列
this.setState(preState => ({
  books: preState.books.slice(1,3);
}))
複製程式碼

當從books中過濾部分元素後,作為新狀態時,使用陣列的filter方法:

// 使用preState、filter建立新陣列
this.setState(preState => ({
  books: preState.books.filter(item => {
    return item != `React`; 
  });
}))
複製程式碼

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

3. 狀態的型別是簡單物件(Plain Object)

如state中有一個狀態owner,結構如下:

this.state = {
  owner = {
    name: `老幹部`,
    age: 30
  }  
}
複製程式碼

當修改state時,有如下兩種方式:

1) 使用ES6 的Object.assgin方法

this.setState(preState => ({
  owner: Object.assign({}, preState.owner, {name: `Jason`});
}))
複製程式碼

2) 使用物件擴充套件語法(object spread properties

this.setState(preState => ({
  owner: {...preState.owner, name: `Jason`};
}))
複製程式碼

總結一下,建立新的狀態的關鍵是,避免使用會直接修改原物件的方法,而是使用可以返回一個新物件的方法。當然,也可以使用一些Immutable的JS庫,如Immutable.js,實現類似的效果。

那麼,為什麼React推薦元件的狀態是不可變物件呢?一方面是因為不可變物件方便管理和除錯,瞭解更多可參考這裡;另一方面是出於效能考慮,當元件狀態都是不可變物件時,我們在元件的shouldComponentUpdate方法中,僅需要比較狀態的引用就可以判斷狀態是否真的改變,從而避免不必要的render方法的呼叫。當我們使用React 提供的PureComponent時,更是要保證元件狀態是不可變物件,否則在元件的shouldComponentUpdate方法中,狀態比較就可能出現錯誤。

下篇預告:

React 深入系列4:元件的生命週期


新書推薦《React進階之路》

作者:徐超

畢業於浙江大學,碩士,資深前端工程師,長期就職於能源物聯網公司遠景智慧。8年軟體開發經驗,熟悉大前端技術,擁有豐富的Web前端和移動端開發經驗,尤其對React技術棧和移動Hybrid開發技術有深入的理解和實踐經驗。

React 深入系列3:Props 和 State

React 深入系列3:Props 和 State

React 深入系列3:Props 和 State

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章