元件狀態(state)是一種持有,處理和使用資訊的方式。state包含的資訊僅作用於一個給定元件的內部,並允許你根據它實現元件的一些邏輯。state通常是一個POJO(Plain Old Java[Script] Object)物件,改變它是使得元件重新render自己的方式之一。
state是react背後原理的重要基礎概念之一,但是它也有一些特點使得它用起來會有點難以捉摸並且有可能會導致在你的應用中出現一些預料之外的行為。
更新State
唯一你能直接寫this.state的地方應該是元件的建構函式中。在其它所有地方你都應該使用this.setState函式,它接受一個物件作為引數,這個物件最終會被合併到元件的當前狀態中。
而在技術上你是可以通過this.state={//a new object}這種方式直接修改狀態的,但是它不會引起元件使用新的值去重新渲染,然後導致狀態不一致的問題。
setState是非同步的
事實上setState會引起的一致性處理(reconciliation)——重新渲染元件樹的過程,是下一個屬性的基礎即setState是非同步的。這允許我們在單個作用域內多次呼叫setState而不會觸發不必要的重新渲染整個樹。
這就是為什麼在你更新state後並不能立馬看見新的值。
// 假設 this.state = { value: 0 }
this.setState({
value: 1
});
console.log(this.state.value); // 0
複製程式碼
React 也會嘗試將多次setState呼叫組合或者批處理為一次呼叫:
// 假設 this.state = { value: 0 };
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});
複製程式碼
在上面所有的呼叫完成後,this.state.value將是1,而不是我們的期望值3。那麼怎麼得到期望值3呢?
setState接受一個函式作為它的引數
如果你傳遞一個函式作為setState的第一個引數,React將會使用在當前呼叫時刻的state去呼叫它並期望你返回一個物件合併到state中。所以你可以把我們上面的程式碼改成下面這樣即可:
// 假設 this.state = { value: 0 };
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));
複製程式碼
這樣this.state.value 的值就是 3了,就和上面我們認為期望值應該是3相一致了。
記住當在更新state為一個值的時候應始終使用這種語法,它的計算是基於前面的一個狀態的。
setState是同步的嗎???
記住你剛才學習到setState是非同步的。事實證明並非始終如此。這取決於執行上下文,請看下面的例子:
render() {
return <button onClick={this.inc}>Click to update</button>
}
inc() {
console.log(`before: ` + this.state.test);
this.setState({
test: this.state.test+1
});
console.log(`after: ` + this.state.test);
}
複製程式碼
點選按鈕元素將會導致你的console中顯示:
// click!
before: 1
after: 1
// click!
before: 2
after: 2
複製程式碼
但如果我們新增以下程式碼:
componentDidMount() {
setInterval(this.inc, 1000);
}
複製程式碼
我們將在console中看到:
before: 1
after: 2
before: 2
after: 3
複製程式碼
因此,我們需要學習什麼時候期望得到哪種行為嗎?並不是如此。假設setState的確是非同步是非常安全的,因為在未來它就是如此。
setState接受一個回撥函式
如果你需要執行一些函式,或者驗證狀態是否真的有更新正確。你還可以給setState傳遞一個函式作為第二個引數,這個函式會在狀態更新完畢後得到執行。請記住由於單個塊內的所有更新會被合併成一個,這將導致每個setState中的回撥中得到的state值是全更新的state。
另外一種可以保證你的程式碼執行是在更新完成以後的方式是將執行程式碼放在componentWillUpdate 或者 componentDidUpdate中。然而對比回撥函式的方式,這兩個方法會在shouldComponentUpdate中阻止你的元件更新時不會被呼叫。
常見錯誤
其中最常見的錯誤之一就是在建構函式中使用props設定state的值。考慮如下程式碼:
class Component extends React.Component {
constructor(props) {
super(props);
this.state = { value: this.props.value };
}
render() {
return <div>The value is: {this.state.value}</div>
}
}
複製程式碼
如果它的父元件這樣render它:
<Component value={42} />
複製程式碼
它將會正確渲染value為42,但如果父元件中修改成如下:
<Component value={13} />
複製程式碼
那它仍會認為this.state.value是42,這是因為React並不會銷燬元件並重新建立它——它會重用一旦渲染好的元件,並且不會重新執行建構函式。要避免這個問題,你應該不要將props賦值給state,而應在render方法中使用this.props.value。
如果你還是想要使用state(如果你的props是以一種非常複雜的計算的使用模式,你不希望每一次render都執行這些複雜的計算),你還可以實現一種在需要的時候才去更新state的解決方案,例如:
class Component extends React.Component {
constructor(props) {
super(props);
this.state = { value: this.props.value };
}
componentWillReceiveProps(nextProps) {
if(nextProps.value !== this.props.value) {
this.setState({value: nextProps.value});
}
}
render() {
return <div>The value is: {this.state.value}</div>
}
}
複製程式碼
請記住任何componentWill*函式都不是一個合適的地方去觸發side effect(如ajax請求),所以請使用
componentDidUpdate(previousProps, previousState),同樣也提供和上面類似的if防護語句確保在沒有變化時不會執行相關程式碼。
寫在最後的話:這篇文章在Medium中獲得超過2.1K的贊,挺不錯的,值得翻譯!
翻譯如有不正,歡迎留言指出!