引言
本节内容介绍React组件中关于状态(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);
在本节中,我们将学习如何封装Clock组件并使其能够被复用。并且Clock组件将会设置自己的计时器并且每秒更新一次。
首先,我们可以封装Clock组件的UI元素。
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);
但是上面这段代码缺少了一个非常重要的环节,那就是Clock组件能够自行设置自己的计时器并且每秒更新一次UI。
理想状态下我们只需要编写下面的代码一次,在此之后Clock组件就能够自动更新UI。
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
为了实现这个功能,我们就需要在Clock组件中加入状态(state)。
状态(state)与props相似,但确是属于组件的私有属性并且完全受控于组件。
将函数组件转化为class组件
我们可以通过以下5个步骤将一个函数组件(如Clock)转化为class组件。
- 创建一个相同名称的ES6 class,并且继承
React.component。 - 在其中添加一个方法
render()。 - 将函数组件中的UI元素添加到
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>
);
}
}
现在Clock组件已经是一个类组件了。
每当UI更新时render方法就会被调用,但是只要在相同的DOM节点渲染Clock组件时,我们仅会用到一个Clock的实例,这就使得我们可以使用如state或者生命周期方法等其他的特性。
向class组件中添加局部state
我们将会通过以下三个步骤将date从props中转移到state中。
1.将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>
);
}
}
2.在class中添加一个class constructor并在其中初始化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的。
constructor(props) {
super(props);
this.state = {date: new Date()};
}
class组件必须创建带有props参数的constructor方法。
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')
);
接下来我们将学习如何让Clock组件设置自己的计时器并且每秒更新一次UI。
向class中添加生命周期方法
如果应用中使用了多个组件,那么在组件被销毁时释放它所占用的资源是十分必要的。
当Clock组件第一次在DOM上渲染时想要设置一个计时器,在React中这称为mounting(挂载)。
当Clock组件所创建的DOM被移出时我们想要清除它的计时器,这在React中称为unmounting(解除挂载)。
我们可以在class组件中声明一些特殊的方法,以此在组件挂载或卸载时运行某些特定的代码。
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()和componentWillUnmount()统称为生命周期方法。
componentDidMount()方法在组件被渲染后调用,适合设置计时器。
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意我们通过this(this.timerID)保存计时器ID。
当你需要存储一些不参与数据流的数据时(如timerID),你可以手动添加额外的字段。
我们将会在componentWillUnmount()生命周期方法中销毁计时器。
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们将要实现tick()方法来使得Clock组件每秒更新一次。
在这里我们使用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')
);
现在这个计时器已经可以每秒更新了。
让我们快速回顾一下这段代码运行过程中发生了说明以及方法的调用顺序。
- 当
<Clock />传递进ReactDOM.render()时,React调用Clock组件的constructor方法。由于现在Clock组件需要展示在页面上,所以它初始化了this.state(包含了当前时间对象),之后我们将更新state。 - 之后React调用Clock组件的
render()方法,这个方法告诉React如何在页面上渲染Clock。React以此更新DOM来匹配Clock的渲染输出。 - 当Clock组件已经在DOM中渲染时,React调用
componentDidMount()方法。在这个方法中,Clock组件设置了一个计时器来每秒调用一次tick()方法。 - 在每秒调用一次的
tick()方法中,Clock组件使用setState()定时更新当前时间。通过调用setState(),React知道了state被改变,于是重新调用render()方法来更新页面。因为此时,在render()方法中的this.state.date与之前不同,所以重新渲染的输出时最新的时间,React也因此更新了它的DOM。 - 如果Clock组件将要从DOM中移除,React将会调用
componentWillUmmount()来终止计时器.
正确地使用state
使用setState()必须知道的三点。
不要直接修改state
以下代码不会重新渲染组件。
// 错误
this.state.comment = 'Hello';
需要使用setState()才能使得组件重新渲染。
// 正确
this.setState({comment: 'Hello'});
能直接给this.state赋值的地方只有个constructor()构造函数。
state更新可能是异步的
出于性能考虑,React可能会把多个setState()调用合并成一个。
由于this.props和this.state可能会异步更新,所以不能根据当前的值计算下一个state的值。
下面的代码就可能会导致更新counter失败。
//错误
this.setState({
counter: this.state.counter + this.props.increment,
});
为了能成功更新counter,我们需要传递一个函数给setState()而不是传递一个对象。这个函数中,上一个state将会作为第一个参数,而此次更新所应用的props作为第二个参数。
// 正确
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
在上面的例子中我们使用的是箭头函数,但是常规的函数也是适用的。
// 正确
this.setState(function(state, props) {
return {
counter: state.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
});
});
}
数据是向下流动的
无论是父组件还是子组件都无法知道一个组件是有状态的还是无状态的,同时也无需关心它是函数组件还是class组件。
这也是state被称为是局部的或是封装的原因。除了拥有这个state的组件外,其他组件是无法获取此state的。
但是组件可以选择把自身的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>;
}
这通常被称为自顶而下或单向 数据流。任意state都是从属于特定的组件。从该state派生的数据或UI只能影响在组件树中位于该组件之下的组件。
如果把组件树当作是props的瀑布流,任意组件的state就像是这个瀑布的补充水源,随着这个瀑布流下去。
为了更好地展示组件的独立性,我们创建一个App组件并在其中渲染三个Clock。
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
运行之后可以看到每一个Clock都是独立更新的。
在React应用中,组件时有状态的还是无状态的属于组件的实现细节,它是会随着时间变化的。你可以在有状态组件中使用无状态组件,反之亦然。