State 和生命周期
考慮前面章節中時鐘的例子。
到目前位置,我們僅學習了一種更新 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上試試。
在這一章節,我們將學習怎樣使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上試試。
然而,這遺漏了重要的一點:事實上,Clock
設置一個計時器并每秒更新 UI 應該是Clock
的實現細節。
理想中,我們想要像下面這樣寫一次,然后Clock
更新它自身:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
為了實現它,我們需要給Clock
組件添加“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>
);
}
}
在CodePen上試試。
Clock
現在被定義為類而不是函數。
這讓我們可以使用額外的特性比如:本地 state 和生命周期鉤子。
添加本地 State 到一個類
我們將通過三步將date
從props
移到state
。
1)在render()
方法中使用this.state.date
替換this.props.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.toLocaleTimeString()}.</h2>
</div>
);
}
}
注意我們是怎樣傳遞props
給基類構造函數的:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
組件類應該總是通過props
調用基類的構造函數。
3)從<Clock />
元素移除date
屬性:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
我們稍后將定時器代碼添加回組件本身。
最終看起來是這個樣子:
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')
);
在CodePen上試試。
接下來,我們將使Clock
設置自己的計時器,并每秒更新它自身。
添加生命周期方法到一個類
在應用中有很多組件,當它們被銷毀時,釋放這些組件所占用的資源是很重要的。
我們想要每當Clock
被首次渲染到 DOM時設置一個計時器。這在 React 中稱為“掛載”。
我們也想每當通過Clock
生成的 DOM 被移除時清除這個計時器。這在 React 總稱為“卸載”。
當一個組件在掛載和卸載的時候,我們可以在組件類中聲明特殊的方法來運行一些代碼:
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
);
}
注意我們如何在this
的右邊保存計時器的 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')
);
在CodePen上試試。
現在時鐘每秒鐘都會“嘀嗒”一次.
讓我們快速回顧發生了什么,以及方法被調用的順序:
- 當
<Clock />
被傳遞到ReactDOM.render()
時,React 調用Clock
組件的構造函數。由于Clock
需要顯示當前的時間,我們使用一個包含當前時間的對象初始化this.state
。我們稍后將更新這個 state。 - React 接著調用
Clock
組件的render()
方法。在這里 React 知道了應該在屏幕上顯示什么。React 接著更新 DOM 來匹配Clock
的渲染輸出。 - 當
Clock
輸出被插入進 DOM 時,React 調用componentDidMount()
生命周期鉤子。在其內部,Clock
組件要求瀏覽器設置一個計時器來一秒鐘調用一次tick()
。 - 每一秒瀏覽器調用一次
tick()
方法。在其內部,Clock
組件通過調用一個使用包含當前時間的對象作為參數的setState()
方法來安排 UI 的更新。由于setState()
的調用,React 知道了 state 已經改變,并且再次調用render()
方法獲取應該在屏幕上顯示的內容。這一次,render()
方法中的this.state.date
是不同的,所以渲染輸出會包含更新的時間。React 相應的更新 DOM。 - 如果
Clock
組件永久的從 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。
例如,下面這段代碼更新counter
會失敗:
// 錯誤
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 + props.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 作為 props 向下傳遞給它的子組件。
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
這也適用于用戶自定義組件:
<FormattedDate date={this.state.date} />
FormattedDate
組件會接收date
作為它的 props,并且不會知道它是否來自Clock
的 state、props,還是手動輸入的。
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
在CodePen上試試。
這通常被稱為“自上而下”的或“單向”數據流。任何 state 始終隸屬于一些特定的組件,并且任何來源于這個 state 的數據或 UI 只能影響到組件樹“下面”的組件。
如果你把組件樹想象成一個由 props 構成的瀑布,每一個組件的 sate 就像額外的水源,會在任意的節點匯入這個瀑布并且隨之向下流。
為了展示所有的組件都是真正的獨立的,我們可以創建一個App
組件,渲染三個<Clock>
:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
在CodePen上試試。
每個Clock
設置它自己的定時器,并獨立更新。
在 React 應用中,不管組件是有狀態的還是無狀態的,都被認為組件的實現細節可以隨時間發生改變的。你可以在有狀態的組件內使用無狀態的組件,反之亦然。
下一步
事件處理