想一下上一節中那個滴答計時的例子。
迄今為止,我們只學到一種更新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
去更新自己:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
為了實現這個功能,我們需要為Clock
組件添加state
。
State和props類似,但他是私有的,并完全由組件控制。
正如我們之前提到的,使用類定義的組件有一些額外的特性。本地的state就是這樣一個特性:只能通過類來開啟。
將函數轉換成類
你可以在五個步驟內將一個像Clock
一樣的功能化組件轉為一個類:
- 創建一個擴展自
React.Component
的同名ES6類。 - 添加一個名為
render()
空的方法。 - 將函數的內容移到
render()
方法中。 - render()中的
props
替換成this.props
。 - 刪除剩下的空函數聲明。
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
在CodePen上試一試
Clock
現在就是通過類來定義的,而非函數嘍。
現在我們就可以添加諸如本地狀態和生命周期鉤子等額外的特性了。
向類中添加本地狀態
我們將date從props中移動到狀態中:
- 將
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>
);
}
}
- 添加一個類的構造函數,初始化
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
。
- 將prop
date
從<Clock />
元素中移除:
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
);
}
注意我們如何將計時器的ID保存到this
上的。
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')
);
現在時鐘每秒滴答一次。
讓我們快速的回顧下發生了什么,還有這些方法調用的順序:
- 當<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才得知狀態發生了變化,然后去再次調用render()
方法,從而知道哪些東西應該放在屏幕上。這次,render()
方法中的this.state.date
將會不同,所以渲染結果將包含更新后的時間。React也會相應的更新DOM。 - 如果
Clock
組件從DOM中刪除,React將會調用componentWillUnmount()
生命周期鉤子,這樣計時器就停止了。
正確地使用State
關于setState()
,你需要知道三件事情。
不要直接修改State
比如,這個組件就不會重新渲染:
// Wrong
this.state.comment = 'Hello';
用setState()
來代替:
// Correct
this.setState({comment: 'Hello'});
唯一一個你可以給this.state
賦值的地方就是構造函數。
狀態更新可能是異步的
React為了性能,可能將多個setState()
放在一起進行更新。
由于this.props
和this.state
可能不同時更新,你不該依賴這些值來計算下一個狀態。
比如,下面更新計數器的代碼可能會失效:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
使用setState()
的第二種形式(參數是一個函數,而不是一個對象)可以修復這種情況。這個函數將上個狀態作為第一個參數,這次更新時的props作為第二個參數:
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
上面我們用到了箭頭函數,但用普通的函數也可以。
// Correct
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
合并狀態更新
當你調用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
。
向下的數據流
一個組件的父子都不會知道該組件是有狀態的還是無狀態的,而且他們不會去關心它是以函數還是類的形式定義的。
這就是為什么狀態被稱作本地或封裝的。只有擁有、設置它的組件才可以訪問它。
一個組件可以選擇將它的狀態作為props向下傳給它的子組件:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
這對自定義組件同樣有效:
<FormattedDate date={this.state.date} />
FormattedDate
組件從它的props中接收date
,而他并不知道這個數據來源到底是Clock
的狀態,還是Clock
的props,亦或手動輸入的:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
在CodePen上試一試
這通常被稱為“自上而下”或“單向”數據流。任何狀態始終由某個特定組件擁有,從該狀態導出的數據或UI只能影響樹狀結構中他下面的組件。
如果你將一個組件樹想象成一個props的瀑布,那么每個組件的狀態就像一個額外的水源,它可以在任意一點加入,但也是向下流動。
為了表明組件間真的都是獨立的,我們可以創建一個渲染三個<Clock>
的App
組件:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
在CodePen上試一試
每個Clock
設置自己的計時器,并且獨立更新。
在React應用中,組件是否有狀態,是作為組件的實現細節來考慮的,可能隨著時間會發生變化。你可以在有狀態的組件中使用無狀態的組件,反之亦然。