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中的某個狀態發生變化,我們應該重新建立這個狀態物件,而不是直接修改原來的狀態。那麼,當狀態發生變化時,如何建立新的狀態呢?根據狀態的型別,可以分成三種情況:
狀態的型別是不可變型別(數字,字串,布林值,null, undefined)
這種情況最簡單,因為狀態是不可變型別,直接給要修改的狀態賦一個新值即可。如要修改count(數字型別)、title(字串型別)、success(布林型別)三個狀態:this.setState({ count: 1, title: 'Redux', success: true })
狀態的型別是陣列
如有一個數組型別的狀態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.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'}; }))