# 狀態和生命周期
思考前面章節中提到的時鐘的例子。
迄今我們只了解了一種更新 UI 的方式。
我們通過調用 ReactDOM.render() 方法來更新渲染輸出:
~~~
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/gwoJZk?editors=0010)。
在本節中,我們將會了解如何使 Clock 組件真正可復用和封裝。它將設置自己的時鐘,并在每秒更新自身。
我們從封裝時鐘的外觀開始:
~~~
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);
~~~
在 CodePen 中[打開查看它](http://codepen.io/gaearon/pen/dpdoYR?editors=0010)。
然而,它丟失了一個重要的需求:事實是, Clock 設置一個時鐘并每秒更新 UI 應該是 Clock 的實現細節。
理想情況下,我們希望只編寫一次,使 Clock 更新它自己:
~~~
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
~~~
要實現這點,我們需要添加 “state” 到 Clock 組件。
狀態和 props 類似,但是它是私有的,并且被組件完全控制。
我們之前提到的,組件定義為類有一些額外的功能。就是局部狀態:只有類組件可以用的特性。
## 轉換功能組件為類組件
可以通過五部轉換一個像 Clock 這樣的功能組件為類組件:
1. 創建一個繼承 React.Component 類的 ES6 同名類
2. 添加一個空方法名為 render()
3. 把函數體移動到 render() 方法
4. 在 render() 方法中使用 this.props 替代 props
5. 刪除保留的空函數聲明
~~~
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/zKRGpo?editors=0010)。
Clock 現在被定義為一個 類組件 而不是功能組件。
這使我們可以使用如局部狀態和生命周期鉤子的額外功能。
## 向一個類組件添加局部狀態
在有許多組件的應用中,非常重要的一點是當組件被銷毀的時候要釋放它們使用的資源。
我們希望無論何時 Clock 被首次渲染到 DOM 時[設置一個時鐘](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval)。這在 React 中被稱為 “mounting”。
另外我們還希望當 Clock 生成的 DOM 被移除時[清除這個時鐘](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval)。在 React 中這叫做“unmounting”。
我們可以在組件類上聲明特定的方法,當組件 mounts 或者 unmouts 時運行一些代碼。
~~~
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() 來安排組件局部狀態的更新:
~~~
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')
);
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/amqdNA?editors=0010)。
現在 Clock 的 tick() 將在每秒運行。
讓我們快速回顧以下其中的過程,和方法被調用的順序:
1. 當 `<Clock />` 被傳遞到 ReactDOM.render(), React 調用 Clock 組件的構造函數。由于 Clock 需要顯示當前時間,它使用一個包含當前時間的對象初始化了 this.state。我們之后會更新這個狀態。
2. React 之后會調用 Clock 組件的 render() 方法。這是 React 知道該顯示什么到屏幕的原因。React 之后匹配 Clock 的 render 輸出的內容來更新 DOM。
3. 當 Clock 輸出被插入到 DOM, React 調用 componentDidMount() 生命周期鉤子。其中,Clock 組件要求瀏覽器設置一個計時器來在每秒調用一次 tick() 。
4. 瀏覽器每秒都會調用 tick() 方法。在這里面, Clock 組件通過調用 setState() 并傳遞一個包含當前時間的對象來安排一個 UI 的更新。得益于 setState() 的調用,React 知道狀態被改變了,然后再次調用 render() 方法來了解什么應該顯示在屏幕中。這次,在render() 方法中的 this.state.date 將是不同的,所以 render 輸出中會包含更新的時間。React 對 DOM 進行相應的更新。
5. 如果 Clock 組件被從 DOM 中移除,React 調用 componentWillUnmount() 生命周期鉤子,所以計時器也會被停止。
## 正確的使用狀態
關于 setState() 有三件事是你應該知道的。
### 1.不要直接修改 state
例如,這將導致不能重新渲染組件:
~~~
// 錯誤用法
this.state.comment = 'Hello';
~~~
而是使用 setState() 替代:
~~~
// 正確用法
this.setState({comment: 'Hello'});
~~~
賦值 this.state 只有一個正確的地點,就是 constructor 中。
### 2. 狀態更新可能是異步的
React 可能為了改進性能而批次處理多個 setState() 到一次更新。
因為 this.props 和 this.state 可能是異步更新的,你不能依賴他們的值計算下一個狀態。
例如,這段代碼可能導致更新 counter 失敗:
~~~
// 錯誤
this.setState({
counter: this.state.counter + this.props.increment,
});
~~~
要彌補這個問題,使用另一種 setState() 的形式,它接受一個函數而不是一個對象。這個函數將接收前一個狀態作為第一個參數,應用更新時的 props 作為第二個參數:
~~~
// 正確
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
~~~
我們在上面使用了一個[箭頭函數](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions),但是也可以使用一個常規的函數:
~~~
// 正確
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
~~~
### 3.狀態更新會被合并
當你調用 setState(), React 將合并你提供的對象到當前的狀態。
例如,你的狀態可能包含幾個獨立的變量:
~~~
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 經常稱為局部或者封裝的原因。它不能被除了擁有并設置它的另外的任何組件訪問。
一個組件可以選擇向下傳遞它的狀態作為它的子組件的 props :
~~~
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
~~~
對于用戶定義的組件也同樣:
~~~
<FormattedDate date={this.state.date} />
~~~
FormattedDate 組件可以接受它的 props 中的 date ,并不能知道它是否來自 Clock 的 state、props 或者是手動創建:
~~~
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/zKRqNB?editors=0010)。
這通常稱為一個“從上到下”或者“單向”的數據流。任何狀態總是被某個特定的組件所有,任何被這個狀態驅動的數據或者 UI 都只影響樹中“下方”的組件。
如果你設想一個組件樹作為一個瀑布式的 props,每個組件的狀態都像一個額外的水源,然后在任意點匯入它,但是同樣只能向下流。
要展示這個,所有組件都是完全獨立的,我們一個 App 組件來渲染三個 `<Clock>`:
~~~
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/vXdGmd?editors=0010)。
每個 Clock 都設置它自己的計時器并獨立更新。
在 React App 中,一個組件是否是有狀態或者無狀態的,被認為是組件的一個實現細節,隨著時間推移可能發生改變。你可以在有狀態的組件中使用無狀態組件,反之亦然。