State和生命週期
考慮前面部分中的滴答時鐘示例(第三章)。
到目前為止,我們只學習了一種更新UI的方法。
我們呼叫ReactDOM.render()
來改變渲染輸出:
import React from `react`;
import ReactDOM from `react-dom`;
function tick() {
const element = (
<div>
<h1>Hell world</h1>
<h2>It is {new Date().toLocaleTimeString()}</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById(`root`)
);
}
setInterval(tick, 1000);
在本節中,我們將學習如何使Clock
元件真正可重用和封裝。 它將設定自己的計時器並每秒更新一次。
我們可以從封裝時鐘的外觀開始:
import React from `react`;
import ReactDOM from `react-dom`;
function Clock(props) {
return (
<div>
<h1>hello world</h1>
<h2>It is {props.date.toLocaleTimeString()}</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById(`root`)
);
}
setInterval(tick, 1000);
然而,它缺少了一個關鍵要求:時鐘設定一個定時器和每秒更新UI的事實應該是時鐘的實現細節。
理想情況下,我們要寫這一次,並由時鐘本身來更新時間:
ReactDOM.render(
<Clock />,
document.getElementById(`root`)
);
要實現這一點,我們需要新增“state”到時鐘元件。
state類似於props,但它是私有的,完全由元件控制。
我們之前提到,定義為類元件具有一些附加功能。 內部state就是:一個只有類元件可用的功能。
將函式形式元件改為類形式元件
您可以通過五個步驟將功能元件(如Clock)轉換為類元件 :
-
建立一個與擴充套件
React.Component
相同名稱的ES6類。 -
為它新增一個單一的空方法
render()
。 -
將函式的主體移動到
render()
方法中。 -
在
render()
主體中用this.props
替換props
。 -
刪除剩餘的空函式宣告。
class Clock extends React.Component {
render() {
return (
<div>
<h1>hello world</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
)
};
}
Clock
現在已經重新定義為類元件而不是之前的功能元件了。
這使我們可以使用額外的功能,如內部state和生命週期鉤子。
向類元件中新增state
我們將分為三個步驟把date
從props
移動到state
:
1)在render()
方法中將this.props.date
替換為this.state.date
:
class Clock extends React.Component {
render() {
return (
<div>
<h1>hello world</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
2)新增一個賦值初始this.state
的類建構函式:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>hello world</h1>
<h2>It is {this.state.date.toLocalTimeString()}.</h2>
</div>
);
}
}
注意我們如何將props傳遞給基類的建構函式:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
類元件應該總是用props呼叫基類建構函式。
3)從<Clock />
元素中刪除date
prop:
ReactDOM.render(
<Clock />,
document.getElementById(`root`)
);
我們稍後將定時器程式碼新增回元件本身。
結果如下所示:
import React from `react`;
import ReactDOM from `react-dom`;
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>hello world</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById(`root`)
);
接下來,我們將使時鐘設定自己的定時器,並每秒更新一次。
向類中新增宣告週期方法
在具有許多元件的應用程式中,釋放元件在銷燬時佔用的資源非常重要。
我們想要在第一次將時鐘渲染到DOM時設定一個計時器。 這在React中稱為“安裝(mounting)”
。
我們還想清除定時器,當時鍾產生的DOM被刪除。 這在React中稱為“解除安裝(unmounting)"
。
我們可以在元件類上宣告特殊方法,以便在元件裝入和解除安裝時執行一些程式碼:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
// 元件已經安裝完畢
}
componentWillUnmount() {
// 元件將要被解除安裝
}
render() {
return (
<div>
<h1>hello world</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
這些方法稱為“生命週期鉤子”
。componentDidMount()
子在元件輸出呈現到DOM之後執行。 這是設定計時器的好地方:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
)
}
注意我們如何儲存計時器ID就在這。
雖然this.props
是由React本身設定的,並且this.state
有一個特殊的含義,如果你需要儲存不用於視覺輸出的東西,你可以手動地新增額外的欄位到類中。
如果你不使用render()
中的東西,它不應該放置在state
中。
我們將拆除componentWillUnmount()
生命週期鉤子中的計時器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最後,我們將實現每秒執行的tick()
方法。
它將使用this.setState()
來排程元件本地state的更新:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
)
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>hello world</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById(`root`)
);
現在時鐘每秒鐘都在滴答地走,棒不棒。。。。
讓我們快速回顧一下發生了什麼以及呼叫方法的順序:
-
1)當將
<Clock />
傳遞給ReactDOM.render()時,React呼叫Clock元件的建構函式。由於Clock需要顯示當前時間,它使用包括當前時間的物件初始化this.state
。我們稍後將更新此state。 -
2)React然後呼叫Clock元件的
render()
方法。這是React如何學習應該在螢幕上顯示什麼。 React然後更新DOM以匹配時鐘的渲染輸出。 -
3)當時鍾輸出插入到DOM中時,React呼叫
componentDidMount()
生命週期鉤子。在其中,時鐘元件要求瀏覽器設定一個定時器,每秒呼叫tick()
一次。 -
4)每秒鐘瀏覽器呼叫
tick()
方法。在其中,Clock元件通過呼叫setState()
和包含當前時間的物件來排程UI更新。由於setState()
呼叫,React知道state已更改,並再次呼叫render()
方法來了解螢幕上應該顯示的內容。這個時候,render()
方法中的this.state.date
將會不同,因此渲染輸出將包括更新的時間。 React相應地更新DOM。 -
5)如果時鐘元件從DOM中被移除,React將呼叫
componentWillUnmount()
生命週期鉤子,因此定時器停止。
正確使用state
關於setState()
你應該瞭解三件事情:
不要直接修改state
例如,這將不會重新渲染元件:
// 這是錯誤的
this.state.comment = `hello`;
應該使用setState()
代替:
// 這是正確的
this.setState({comment: `hello`});
唯一可以分配this.state
的地方是建構函式。
state更新可能是非同步的
React可以將多個setState()
用批處理為單個更新以實現較高的效能。
因為this.props
和this.state
可能是非同步更新的,你不應該依賴它們的值來計算下一個state。
例如,此程式碼可能無法更新計數器:
// 這是錯誤的
this.setState({
counter: this.state.counter + this.props.increment,
});
要解決它,應該使用回撥函式而不是物件來呼叫setState()
。 回撥函式將接收先前的state作為第一個引數,並將應用更新時的props
作為第二個引數:
// 這是正確的
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
我們使用上面的箭頭函式,但它也可以與常規函式一起使用:
// 這同樣也是正確的,將剪頭函式改為普通函式
this.setState(function(prevState, props) {
return {
counter: prevState.counter + prps.increment
}
});
state更新是經過合併的
當呼叫setState()
時,React會將您提供的物件合併到當前state。
例如,您的state可能包含幾個獨立變數:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
}
}
然後,您可以使用單獨的setState()
來獨立地更新它們:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
}});
});
}
合併很淺,所以this.setState({comments})
不會波及this.state.posts
。僅僅只是完全替換了this.state.comments
而已。
資料是向下流動的
父元件和子元件都不能知道某個元件是有State的還是無State的,並且它們不應該關心它是否為功能元件或類元件。
這就是為什麼State通常被設定為區域性變數或封裝到元件內部。 除了擁有和設定它的元件之外的其他任何元件都不能訪問它。
元件可以選擇將其state作為props傳遞給其子元件:
<h2>Is is {this.state.date.toLocaleTimeString()}.</h2>
這也適用於使用者定義的元件:
<formattedDate date={this.state.data} />
FormattedDate
元件將在其props
中接收date
,並且不知道它是來自時鐘的state
,props
還是手動輸入
:
function FormattedData(props) {
return <h2>Is is {props.date.toLocaleTimeString()}.</h2>;
}
這通常被稱為“自頂向下”
或“單向”
資料流。 任何state總是由一些特定元件擁有,並且從該state派生的任何資料或UI只能影響樹中的“下面”元件。
如果你想象一個元件樹作為props的瀑布流,每個元件的state就像一個額外的水源,它可以在任意點連線它,但也向下流。
為了顯示所有元件都是真正隔離的,我們可以建立一個App元件來渲染三個<Clock>
:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById(`root`)
);
每個時鐘設定自己的定時器並獨立更新。
在React應用程式中,元件是有狀態還是無狀態被視為可能隨時間更改的元件的實現細節。 您可以在有狀態元件內使用無狀態元件,反之亦然。