想一下上一節(jié)中那個滴答計時的例子莺禁。
迄今為止,我們只學(xué)到一種更新UI的方法狞换。
我們通過調(diào)用ReactDOM.render()
來改變已經(jīng)渲染的輸出:
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上試一試
在本節(jié)里谭网,我們將學(xué)會做一個真正可重用且有封裝良好的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上試一試
不過,上面的代碼漏了一個關(guān)鍵的需求:Clock
應(yīng)該自己去設(shè)置計時器静秆,并且每秒更新UI粮揉。
理想情況下,我們想要只寫一次诡宗,并且讓Clock
去更新自己:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
為了實現(xiàn)這個功能滔蝉,我們需要為Clock
組件添加state
。
State和props類似塔沃,但他是私有的蝠引,并完全由組件控制阳谍。
正如我們之前提到的,使用類定義的組件有一些額外的特性螃概。本地的state就是這樣一個特性:只能通過類來開啟矫夯。
將函數(shù)轉(zhuǎn)換成類
你可以在五個步驟內(nèi)將一個像Clock
一樣的功能化組件轉(zhuǎn)為一個類:
- 創(chuàng)建一個擴(kuò)展自
React.Component
的同名ES6類。 - 添加一個名為
render()
空的方法吊洼。 - 將函數(shù)的內(nèi)容移到
render()
方法中训貌。 - render()中的
props
替換成this.props
。 - 刪除剩下的空函數(shù)聲明冒窍。
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
在CodePen上試一試
Clock
現(xiàn)在就是通過類來定義的递沪,而非函數(shù)嘍。
現(xiàn)在我們就可以添加諸如本地狀態(tài)和生命周期鉤子等額外的特性了综液。
向類中添加本地狀態(tài)
我們將date從props中移動到狀態(tài)中:
- 將
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>
);
}
}
- 添加一個類的構(gòu)造函數(shù)款慨,初始化
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
傳給父類構(gòu)造函數(shù)的:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
組件類應(yīng)該始終調(diào)用父類的構(gòu)造函數(shù),并傳入props
谬莹。
- 將prop
date
從<Clock />
元素中移除:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
稍后我們將計時器代碼加回組件內(nèi)檩奠。
現(xiàn)在結(jié)果看起來:
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
設(shè)置它自己的計時器,每秒自己去進(jìn)行更新附帽。
在類中添加生命周期方法
在有很多組件的應(yīng)用中埠戳,當(dāng)組件被銷毀時,組件可以釋放自己管理的資源是非常的重要蕉扮。
我們希望Clock
在第一次被渲染到DOM時設(shè)置一個計時器整胃。這在React中被稱作"掛載"。
同樣的慢显,我們也希望在生成Clock
被移除的DOM時爪模,清除掉這個計時器.這在React中被稱為"取消掛載"。
我們可以在組件類里聲明一些特殊的方法荚藻,在組件掛載和取消掛載的時候執(zhí)行一些代碼:
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后被執(zhí)行。在這里設(shè)置計時器非常合適:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意我們?nèi)绾螌⒂嫊r器的ID保存到this
上的应狱。
this.props
由React自己來設(shè)置共郭,this.state
有特殊的意義,除此之外疾呻,如果你需要存一些不用于顯示的東西除嘹,可以自由地添加這些字段到類上面。
不再render()
中使用的東西岸蜗,就不應(yīng)該將它們加入state尉咕。
我們將在生命周期鉤子componentWillUnmount()
中拆除這個計時器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最終,我們來實現(xiàn)每秒運行的這個tick()
方法璃岳。
它將使用this.setState()
來定時更新組件的本地狀態(tài):
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')
);
現(xiàn)在時鐘每秒滴答一次年缎。
讓我們快速的回顧下發(fā)生了什么悔捶,還有這些方法調(diào)用的順序:
- 當(dāng)<Clock />傳給
ReactDOM.render()
時,React調(diào)用Clock
組件的構(gòu)造函數(shù)单芜。因為Clock
需要顯示當(dāng)前的時間蜕该,他使用一個包含當(dāng)前時間的對象來初始化this.state
。我們稍后更新這個state洲鸠。 - 接著堂淡,React調(diào)用
Clock
組件的render()
方法。React由此得知那些東西應(yīng)該顯示在屏幕上扒腕。然后React更新DOM來使之匹配Clock
的渲染輸出绢淀。 - 當(dāng)
Clock
的輸出被插入DOM中,React調(diào)用componentDidMount()
生命周期鉤子袜匿。在這個鉤子中更啄,Clock
組件將請求瀏覽器設(shè)置一個計時器每秒調(diào)用tick()
稚疹。 - 瀏覽器每秒調(diào)用一次
tick()
方法居灯。在這個方法中,Clock
組件通過調(diào)用setState()
(傳入一個包含當(dāng)前時間的對象)内狗,來調(diào)度UI的更新怪嫌。就是因為調(diào)用了setState()
,React才得知狀態(tài)發(fā)生了變化柳沙,然后去再次調(diào)用render()
方法岩灭,從而知道哪些東西應(yīng)該放在屏幕上。這次赂鲤,render()
方法中的this.state.date
將會不同噪径,所以渲染結(jié)果將包含更新后的時間。React也會相應(yīng)的更新DOM数初。 - 如果
Clock
組件從DOM中刪除找爱,React將會調(diào)用componentWillUnmount()
生命周期鉤子,這樣計時器就停止了泡孩。
正確地使用State
關(guān)于setState()
车摄,你需要知道三件事情。
不要直接修改State
比如仑鸥,這個組件就不會重新渲染:
// Wrong
this.state.comment = 'Hello';
用setState()
來代替:
// Correct
this.setState({comment: 'Hello'});
唯一一個你可以給this.state
賦值的地方就是構(gòu)造函數(shù)吮播。
狀態(tài)更新可能是異步的
React為了性能,可能將多個setState()
放在一起進(jìn)行更新眼俊。
由于this.props
和this.state
可能不同時更新意狠,你不該依賴這些值來計算下一個狀態(tài)。
比如疮胖,下面更新計數(shù)器的代碼可能會失效:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
使用setState()
的第二種形式(參數(shù)是一個函數(shù)环戈,而不是一個對象)可以修復(fù)這種情況誊役。這個函數(shù)將上個狀態(tài)作為第一個參數(shù),這次更新時的props作為第二個參數(shù):
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
上面我們用到了箭頭函數(shù)谷市,但用普通的函數(shù)也可以蛔垢。
// Correct
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
合并狀態(tài)更新
當(dāng)你調(diào)用setState()
,React將你提供的對象合并到當(dāng)前狀態(tài)迫悠。
舉例來說鹏漆,你的狀態(tài)可能包含幾個獨立的變量:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
你可以多次調(diào)用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
艺玲。
向下的數(shù)據(jù)流
一個組件的父子都不會知道該組件是有狀態(tài)的還是無狀態(tài)的,而且他們不會去關(guān)心它是以函數(shù)還是類的形式定義的鞠抑。
這就是為什么狀態(tài)被稱作本地或封裝的饭聚。只有擁有、設(shè)置它的組件才可以訪問它搁拙。
一個組件可以選擇將它的狀態(tài)作為props向下傳給它的子組件:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
這對自定義組件同樣有效:
<FormattedDate date={this.state.date} />
FormattedDate
組件從它的props中接收date
秒梳,而他并不知道這個數(shù)據(jù)來源到底是Clock
的狀態(tài)彭雾,還是Clock
的props函喉,亦或手動輸入的:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
在CodePen上試一試
這通常被稱為“自上而下”或“單向”數(shù)據(jù)流。任何狀態(tài)始終由某個特定組件擁有么夫,從該狀態(tài)導(dǎo)出的數(shù)據(jù)或UI只能影響樹狀結(jié)構(gòu)中他下面的組件盐茎。
如果你將一個組件樹想象成一個props的瀑布兴垦,那么每個組件的狀態(tài)就像一個額外的水源,它可以在任意一點加入字柠,但也是向下流動探越。
為了表明組件間真的都是獨立的,我們可以創(chuàng)建一個渲染三個<Clock>
的App
組件:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
在CodePen上試一試
每個Clock
設(shè)置自己的計時器窑业,并且獨立更新钦幔。
在React應(yīng)用中,組件是否有狀態(tài)数冬,是作為組件的實現(xiàn)細(xì)節(jié)來考慮的节槐,可能隨著時間會發(fā)生變化。你可以在有狀態(tài)的組件中使用無狀態(tài)的組件拐纱,反之亦然铜异。