React 的核心思想是組件化的思想,應用由組件搭建而成,而組件中最重要的概念是State(狀態),State是一個組件的UI數據模型,是組件渲染時的數據依據。
一. 如何定義State
定義一個合適的State,是正確創建組件的第一步。State必須能代表一個組件UI呈現的完整狀態集,即組件的任何UI改變,都可以從State的變化中反映出來;同時,State還必須是代表一個組件UI呈現的最小狀態集,即State中的所有狀態都是用于反映組件UI的變化,沒有任何多余的狀態,也不需要通過其他狀態計算而來的中間狀態。
組件中用到的一個變量是不是應該作為組件State,可以通過下面的4條依據進行判斷:
- 這個變量是否是通過Props從父組件中獲?。咳绻牵敲此皇且粋€狀態。
- 這個變量是否在組件的整個生命周期中都保持不變?如果是,那么它不是一個狀態。
- 這個變量是否可以通過其他狀態(State)或者屬性(Props)計算得到?如果是,那么它不是一個狀態。
- 這個變量是否在組件的render方法中使用?如果不是,那么它不是一個狀態。這種情況下,這個變量更適合定義為組件的一個普通屬性,例如組件中用到的定時器,就應該直接定義為this.timer,而不是this.state.timer。
請務必牢記,并不是組件中用到的所有變量都是組件的狀態!當存在多個組件共同依賴一個狀態時,一般的做法是狀態上移,將這個狀態放到這幾個組件的公共父組件中。
二. State 與 Props 區別
除了State, 組件的Props也是和組件的UI有關的。他們之間的主要區別是:State是可變的,是組件內部維護的一組用于反映組件UI變化的狀態集合;而Props對于使用它的組件來說,是只讀的,要想修改Props,只能通過該組件的父組件修改。在組件狀態上移的場景中,父組件正是通過子組件的Props, 傳遞給子組件其所需要的狀態。
三. 如何正確修改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修改前的State。另外需要注意的事,同樣不能依賴當前的Props計算下個狀態,因為Props一般也是從父組件的State中獲取,依然無法確定在組件狀態更新時的值。
舉個例子,對于一個電商類應用,在我們的購物車中,當我們點擊一次購買數量按鈕,購買的數量就會加1,如果我們連續點擊了兩次按鈕,就會連續調用兩次this.setState({quantity: this.state.quantity + 1})
,在React合并多次修改為一次的情況下,相當于等價執行了如下代碼:
Object.assign(
previousState,
{quantity: this.state.quantity + 1},
{quantity: this.state.quantity + 1}
)
于是乎,后面的操作覆蓋掉了前面的操作,最終購買的數量只增加了1個。
如果你真的有這樣的需求,可以使用另一個接收一個函數作為參數的setState
,這個函數有兩個參數,第一個是當前最新狀態(本次組件狀態修改后的狀態)的前一個狀態preState(本次組件狀態修改前的狀態),第二個參數是當前最新的屬性props。如下所示:
// 正確
this.setState((preState, props) => ({
counter: preState.quantity + 1;
}))
3. State 的更新是一個淺合并(Shallow Merge)的過程。
當調用setState修改組件狀態時,只需要傳入發生改變的State,而不是組件完整的State,因為組件State的更新是一個淺合并(Shallow Merge)的過程。例如,一個組件的狀態為:
this.state = {
title : 'React',
content : 'React is an wonderful JS library!'
}
當只需要修改狀態title
時,只需要將修改后的title
傳給setState
:
this.setState({title: 'Reactjs'});
React會合并新的title
到原來的組件狀態中,同時保留原有的狀態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):
// 方法一:將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);
}))
當從books中過濾部分元素后,作為新狀態時,使用數組的filter方法:
// 方法一:將state先賦值給另外的變量,然后使用filter創建新數組
var books = this.state.books;
this.setState({
books: books.filter(item => {
return item != 'React';
});
})
// 方法二:使用preState、filter創建新數組
this.setState(preState => ({
books: preState.books.filter(item => {
return item != 'React';
});
}))
注意不要使用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 使用對象擴展語法(object spread properties)
// 方法一:將state先賦值給另外的變量,然后使用對象擴展語法創建新對象
var owner = this.state.owner;
this.setState({
owner: {...owner, name: 'Jason'};
})
// 方法二:使用preState、對象擴展語法創建新對象
this.setState(preState => ({
owner: {...preState.owner, name: 'Jason'};
}))
總結一下,創建新的狀態對象的關鍵是,避免使用會直接修改原對象的方法,而是使用可以返回一個新對象的方法。當然,也可以使用一些Immutable的JS庫,如Immutable.js,實現類似的效果。
那么,為什么React推薦組件的狀態是不可變對象呢?一方面是因為不可變對象方便管理和調試,了解更多可參考這里;另一方面是出于性能考慮,當對象組件狀態都是不可變對象時,我們在組件的shouldComponentUpdate
方法中,僅需要比較狀態的引用就可以判斷狀態是否真的改變,從而避免不必要的render
調用。當我們使用React 提供的PureComponent
時,更是要保證組件狀態是不可變對象,否則在組件的shouldComponentUpdate
方法中,狀態比較就可能出現錯誤,因為PureComponent
執行的是淺比較(比較對象的引用)。
作者:艾特老干部
鏈接:http://www.lxweimin.com/p/c6257cbef1b1
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。