原文發表在我的博客:http://www.erichain.me/2017/04/17/2017-04-17-more-reasonable-setstate/
React 是我做前端以來接觸到的第三個框架(前兩個分別是 Angular 和 Vue),無論是從開發體驗上和效率上,這都是一門非常優秀的框架,非常值得學習。
原諒我說了一些廢話,以下是正文。
借助於 Redux,我們可以輕松的對 React 中的狀態進行管理和維護,同時,React 也為我們提供了組件內的狀態管理的方案,也就是 setState()
。本文不會涉及到 Redux,我們將從 Component 的角度來說明你不知道的以及更合理的 setState()
。
先說說大家都知道的
在 React 文檔的 State and Lifecycle 一章中,其實有明確的說明 setState()
的用法,向 setState()
中傳入一個對象來對已有的 state 進行更新。
比如現在有下面的這樣一段代碼:
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: this.state.count + 1 }; } }
我們如果想要對這個 state 進行更新的話,就可以這樣使用 setState()
:
this.setState({ count: 1 });
你可能不知道的
最基本的用法世人皆知,但是,在 React 的文檔下面,還寫着,處理關於異步更新 state 的問題的時候,就不能簡單地傳入對象來進行更新了。這個時候,需要采用另外一種方式來對 state 進行更新。
setState()
不僅能夠接受一個對象作為參數,還能夠接受一個函數作為參數。函數的參數即為 state 的前一個狀態以及 props。
所以,我們可以向下面這樣來更新 state:
this.setState((prevState, props) => ({ count: prevState.count + 1 }));
這樣寫的話,能夠達到同樣的效果。那么,他們之間有什么區別呢?
區別
我們來詳細探討一下為什么會有兩種設置 state 的方案,他們之間有什么區別,我們應該在何時使用何種方案來更新我們的 state 才是最好的。
此處,為了能夠明確的看出 state 的更新,我們采用一個比較簡單的例子來進行說明。
我們設置一個累加器,在 state 上設置一個 count
屬性,同時,為其增加一個 increment
方法,通過這個 increment
方法來更新 count
。
此處,我們采用給 setState()
傳入對象的方式來更新 state,同時,我們在此處設置每調用一次 increment
方法的時候,就調用兩次 setState()
。具體的原因我們在后文中會講解。
具體的代碼如下:
class IncrementByObject extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.increment = this.increment.bind(this); } // 此處設置調用兩次 setState() increment() { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); } render() { return ( <div> <button onClick={this.increment}>IncrementByObject</button> <span>{this.state.count}</span> </div> ); } } ReactDOM.render( <IncrementByObject />, document.getElementById('root') );
這時候,我們點擊 button 的時候,count
就會更新了。但是,可能與我們所預期的有所差別。我們設置了點擊一次就調用兩次 setState()
,但是,count
每一次卻還是只增加了 1,所以這是為什么呢?
其實,在 React 內部,對於這種情況,采用的是對象合並的操作,就和我們所熟知的 Object.assign()
執行的結果一樣。
比如,我們有以下的代碼:
Object.assign({}, { a: 2, b: 3 }, { a: 1, c: 4 });
那么,我們最終得到的結果將會是 { a: 1, b: 3, c: 4 }
。對象合並的操作,屬性值將會以最后設置的屬性的值為准,如果發現之前存在相同的屬性,那么,這個屬性將會被后設置的屬性所替換。所以,也就不難理解為什么我們調用了兩次 setState()
之后,count
依然只增加了 1 了。
用簡短的代碼說明就是這樣:
this.setState({ count: this.state.count + 1 }); // 同理於 Object.assign({}, this.state, { count: this.state.count + 1 });
以上是我們采用對象的方式傳入 setState()
來更新 state 的說明。接下來我們再看看使用函數的方式來更新 state 會有怎么樣的效果呢?
我們將上面的累加器采用另外的方式來實現一次,在 setState()
的時候,我們采用傳入一個函數的方式來更新我們的 state。
class IncrementByFunction extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.increment = this.increment.bind(this); } increment() { // 采用傳入函數的方式來更新 state this.setState((prevState, props) => ({ count: prevState.count + 1 })); this.setState((prevState, props) => ({ count: prevState.count + 1 })); } render() { return ( <div> <button onClick={this.increment}>IncrementByFunction</button> <span>{this.state.count}</span> </div> ); } } ReactDOM.render( <IncrementByFunction />, document.getElementById('root') );
當我們再次點擊按鈕的時候,就會發現,我們的累加器就會每次增加 2 了。
我們可以通過查看 React 的源代碼來找出這兩種更新 state 的區別 (此處只展示通過傳入函數進行更新的方式的部分源碼)。
在 React 的源代碼中,我們可以看到這樣一句代碼:
this.updater.enqueueSetState(this, partialState, callback, 'setState');
然后,enqueueSetState
函數中又會有這樣的實現:
queue.push(partialState);
enqueueUpdate(internalInstance);
所以,與傳入對象更新 state 的方式不同,我們傳入函數來更新 state 的時候,React 會把我們更新 state 的函數加入到一個隊列里面,然后,按照函數的順序依次調用。同時,為每個函數傳入 state 的前一個狀態,這樣,就能更合理的來更新我們的 state 了。
問題所在
那么,這就是傳入對象來更新 state 會導致的問題嗎?當然,這只是問題之一,還不是主要的問題。
我們之前也說過,我們在處理異步更新的時候,需要用到傳入函數的方式來更新我們的 state。這樣,在更新下一個 state 的時候,我們能夠正確的獲取到之前的 state,並在在其基礎之上進行相應的修改。而不是簡單地執行所謂的對象合並。
所以說,我們建議,在使用 setState
的時候,采用傳入函數來更新 state 的方式,這樣也是一個更合理的方式。
我在 CodePen 上將這兩個效果組合到了一起,感興趣的話,你可以去試着點擊一下。
See the Pen React Functional setState by Erichain (@Erichain) on CodePen.