[英]Why can't I directly modify a component's state, really?
我知道 React 教程和文檔毫不含糊地警告說 state 不應該直接突變,並且一切都應該 go 通過setState
。
我想了解為什么我不能直接更改 state 然后(在同一個函數中)調用this.setState({})
來觸發render
。
例如:下面的代碼似乎工作得很好:
const React = require('react');
const App = React.createClass({
getInitialState: function() {
return {
some: {
rather: {
deeply: {
embedded: {
stuff: 1,
},
},
},
},
},
};
updateCounter: function () {
this.state.some.rather.deeply.embedded.stuff++;
this.setState({}); // just to trigger the render ...
},
render: function() {
return (
<div>
Counter value: {this.state.some.rather.deeply.embedded.stuff}
<br></br>
<button onClick={this.updateCounter}>Increment</button>
</div>
);
},
});
export default App;
我完全贊成遵循約定,但我想進一步了解 ReactJS 的實際工作原理以及 go 可能出現什么錯誤,或者它是否與上述代碼次優。
this.setState
文檔下的注釋基本上確定了兩個問題:
this.setState
這可能會替換(覆蓋?)您所做的突變。 我看不出在上面的代碼中怎么會發生這種情況。setState
可能會以異步/延遲方式有效地改變this.state
,因此在調用this.setState
之后訪問this.state
時,不能保證訪問最終的突變 Z9ED39E2EA931586B6A98576 我明白了,如果 this.setState 是更新this.setState
的最后一次調用,這不是問題。React 遵循單向數據流。 這意味着,react 內部的數據流應該並且將被期望處於循環路徑中。
React 的無通量數據流
為了讓 React 像這樣工作,開發人員讓 React 類似於函數式編程。 函數式編程的經驗法則是不變性。 讓我大聲而清楚地解釋它。
單向流如何工作?
states
是包含組件數據的數據存儲。view
基於狀態呈現。view
需要更改屏幕上的某些內容時,應該從store
提供該值。setState()
函數,該函數接受一個新states
的object
,並對先前的狀態進行比較和合並(類似於object.assign()
),並將新狀態添加到狀態數據存儲中。view
使用的新狀態並將其顯示在屏幕上。此循環將在組件的整個生命周期中持續。
如果你看到上面的步驟,它清楚地表明當你改變狀態時,后面發生了很多事情。 因此,當您直接改變狀態並使用空對象調用setState()
時。 previous state
會被你的突變污染。 因此,兩個狀態的淺層比較和合並將受到干擾或不會發生,因為您現在只有一個狀態。 這將破壞所有 React 的生命周期方法。
結果,您的應用程序將表現異常甚至崩潰。 大多數情況下,它不會影響您的應用程序,因為我們用於測試的所有應用程序都非常小。
JavaScript 中Objects
和Arrays
突變的另一個缺點是,當您分配一個對象或數組時,您只是對該對象或該數組進行引用。 當您對它們進行變異時,對該對象或該數組的所有引用都會受到影響。 React 在后台以一種智能的方式處理這個問題,並簡單地給我們一個 API 讓它工作。
在 React 中處理狀態時最常見的錯誤
// original state
this.state = {
a: [1,2,3,4,5]
}
// changing the state in react
// need to add '6' in the array
// bad approach
const b = this.state.a.push(6)
this.setState({
a: b
})
在上面的例子中, this.state.a.push(6)
將直接改變狀態。 將其分配給另一個變量並調用setState
與下面顯示的相同。 由於我們無論如何都改變了狀態,因此將其分配給另一個變量並使用該變量調用setState
是沒有意義的。
// same as
this.state.a.push(6)
this.setState({})
很多人都這樣做。 這是大錯特錯。 這破壞了 React 的美感並且是不好的編程習慣。
那么,在 React 中處理狀態的最佳方式是什么? 讓我解釋。
當您需要更改現有狀態中的“某物”時,首先從當前狀態獲取該“某物”的副本。
// original state
this.state = {
a: [1,2,3,4,5]
}
// changing the state in react
// need to add '6' in the array
// create a copy of this.state.a
// you can use ES6's destructuring or loadash's _.clone()
const currentStateCopy = [...this.state.a]
現在,改變currentStateCopy
不會改變原始狀態。 對currentStateCopy
進行操作並使用setState()
將其設置為新狀態。
currentStateCopy.push(6)
this.setState({
a: currentStateCopy
})
這很漂亮,對吧?
通過這樣做, this.state.a
的所有引用在我們使用setState
之前都不會受到影響。 這使您可以控制您的代碼,這將幫助您編寫優雅的測試並使您對代碼在生產中的性能充滿信心。
要回答你的問題,
為什么我不能直接修改組件的狀態?
是的,你可以。 但是,您需要面對以下后果。
state
的控制。PS。 我已經編寫了大約 10000 行可變的 React JS 代碼。 如果它現在壞了,我不知道去哪里尋找,因為所有的值都在某個地方發生了變化。 當我意識到這一點時,我開始編寫不可變代碼。 相信我! 這是您可以對產品或應用程序做的最好的事情。
希望這可以幫助!
setState
的 React 文檔有這樣的說法:
永遠不要直接改變
this.state
,因為之后調用setState()
可能會替換你所做的改變。 將this.state
視為不可變的。
setState()
不會立即改變this.state
而是創建一個掛起的狀態轉換。 在調用此方法后訪問this.state
可能會返回現有值。無法保證對
setState
的調用同步操作,並且可能會批處理調用以提高性能。除非在
shouldComponentUpdate()
中實現了條件渲染邏輯,否則setState()
將始終觸發重新渲染。 如果正在使用可變對象並且無法在shouldComponentUpdate()
中實現邏輯,則僅在新狀態與先前狀態不同時調用setState()
將避免不必要的重新渲染。
基本上,如果您直接修改this.state
,您會創建一種情況,這些修改可能會被覆蓋。
與您的擴展問題 1) 和 2) 相關, setState()
不是立即的。 它根據它認為正在發生的事情對狀態轉換進行排隊,其中可能不包括對this.state
的直接更改。 由於它是排隊而不是立即應用的,因此完全有可能在兩者之間修改某些內容,從而覆蓋您的直接更改。
如果不出意外,考慮到不直接修改this.state
可以被視為一種好的做法,您可能會更好。 您可能親自知道您的代碼與 React 交互的方式不會發生這些覆蓋或其他問題,但是您正在創造一種情況,其他開發人員或未來的更新可能會突然發現自己遇到奇怪或微妙的問題。
最簡單的答案“
為什么我不能直接修改組件的狀態:
都是關於更新階段的。
當我們更新組件的狀態時,它的所有子組件也將被渲染。 或者我們渲染的整個組件樹。
但是當我說我們的整個組件樹被渲染時,並不意味着整個 DOM 都被更新了。 當一個組件被渲染時,我們基本上得到了一個反應元素,所以它正在更新我們的虛擬 dom。
React 然后會查看虛擬 DOM,它也有舊虛擬 DOM 的副本,這就是為什么我們不應該直接更新狀態,所以我們可以在內存中有兩個不同的對象引用,我們有舊虛擬 DOM以及新的虛擬 DOM。
然后 react 將找出發生了什么變化,並據此相應地更新真實的 DOM。
希望能幫助到你。
令我驚訝的是,當前的答案都沒有談論純/備忘錄組件( React.PureComponent
或React.memo
)。 這些組件僅在檢測到其中一個道具發生變化時才會重新渲染。
假設您直接改變狀態並將其傳遞給下面的組件,而不是值,而是過度耦合對象。 該對象仍然具有與前一個對象相同的引用,這意味着即使您改變了其中一個屬性,pure/memo 組件也不會重新渲染。
由於在從庫中導入組件時,您並不總是知道您正在使用哪種類型的組件,這是堅持非變異規則的另一個原因。
以下是此行為的示例(使用R.evolve
來簡化創建副本和更新嵌套內容):
class App extends React.Component { state = { some: { rather: { deeply: { nested: { stuff: 1 } } } } }; mutatingIncrement = () => { this.state.some.rather.deeply.nested.stuff++; this.setState({}); } nonMutatingIncrement = () => { this.setState(R.evolve( { some: { rather: { deeply: { nested: { stuff: n => n + 1 } } } } } )); } render() { return ( <div> Normal Component: <CounterDisplay {...this.state} /> <br /> Pure Component: <PureCounterDisplay {...this.state} /> <br /> <button onClick={this.mutatingIncrement}>mutating increment</button> <button onClick={this.nonMutatingIncrement}>non-mutating increment</button> </div> ); } } const CounterDisplay = (props) => ( <React.Fragment> Counter value: {props.some.rather.deeply.nested.stuff} </React.Fragment> ); const PureCounterDisplay = React.memo(CounterDisplay); ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/ramda@0/dist/ramda.min.js"></script> <div id="root"></div>
setState 觸發重新渲染組件。當我們想要一次又一次地更新狀態時,我們必須需要 setState 否則它不能正常工作。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.