想一下上一节中那个滴答计时的例子。
迄今为止,我们只学到一种更新UI的方法。
我们通过调用ReactDOM.render()
来改变已经渲染的输出:
function tick() {
const element = (
Hello, world!
It is {new Date().toLocaleTimeString()}.
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在CodePen上试一试
在本节里,我们将学会做一个真正可重用且有封装良好的Clock
组件。
我们可以从封装时钟的外观开始:
function Clock(props) {
return (
Hello, world!
It is {props.date.toLocaleTimeString()}.
);
}
function tick() {
ReactDOM.render(
,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在CodePen上试一试
不过,上面的代码漏了一个关键的需求:Clock
应该自己去设置计时器,并且每秒更新UI。
理想情况下,我们想要只写一次,并且让Clock
去更新自己:
ReactDOM.render(
,
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 (
Hello, world!
It is {this.props.date.toLocaleTimeString()}.
);
}
}
在CodePen上试一试
Clock
现在就是通过类来定义的,而非函数喽。
现在我们就可以添加诸如本地状态和生命周期钩子等额外的特性了。
向类中添加本地状态
我们将date从props中移动到状态中:
- 将
render()
方法中的this.props.date
替换为this.state.date
:
class Clock extends React.Component {
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
- 添加一个类的构造函数,初始化
this.state
:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
注意我们是如何将props
传给父类构造函数的:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
组件类应该始终调用父类的构造函数,并传入props
。
- 将prop
date
从
元素中移除:
ReactDOM.render(
,
document.getElementById('root')
);
稍后我们将计时器代码加回组件内。
现在结果看起来:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
ReactDOM.render(
,
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 (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
这些方法称为"生命周期钩子"。
钩子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 (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
在CodePen上试一试
现在时钟每秒滴答一次。
让我们快速的回顾下发生了什么,还有这些方法调用的顺序:
- 当
传给 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向下传给它的子组件:
It is {this.state.date.toLocaleTimeString()}.
这对自定义组件同样有效:
FormattedDate
组件从它的props中接收date
,而他并不知道这个数据来源到底是Clock
的状态,还是Clock
的props,亦或手动输入的:
function FormattedDate(props) {
return It is {props.date.toLocaleTimeString()}.
;
}
在CodePen上试一试
这通常被称为“自上而下”或“单向”数据流。任何状态始终由某个特定组件拥有,从该状态导出的数据或UI只能影响树状结构中他下面的组件。
如果你将一个组件树想象成一个props的瀑布,那么每个组件的状态就像一个额外的水源,它可以在任意一点加入,但也是向下流动。
为了表明组件间真的都是独立的,我们可以创建一个渲染三个
的App
组件:
function App() {
return (
);
}
ReactDOM.render(
,
document.getElementById('root')
);
在CodePen上试一试
每个Clock
设置自己的计时器,并且独立更新。
在React应用中,组件是否有状态,是作为组件的实现细节来考虑的,可能随着时间会发生变化。你可以在有状态的组件中使用无状态的组件,反之亦然。