状态(State) 和 生命周期
本页面介绍 React 组件中 状态(state) 和生命周期的概念。 你可以在这里找到 详细的组件API参考。
考虑 前面章节 中的滴答时钟示例。
在 元素渲染 中,我们只学会了一种更新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
组件变得真正可复用 和 封装的更好。它将设置自己的计时器,并在每秒更新自身。
我们可以从封装时钟开始:
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
自身实现的一部分。
理想情况下,我们应该只引用一个 Clock
, 然后让它自动计时并更新:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
要实现这点,我们需要添加 state
到 Clock
组件。
state
和 props
类似,但是它是私有的,并且由组件本身完全控制。
我们 之前提到过, 用类定义的组件有一些额外的特性。 这个”类专有的特性”, 指的就是局部状态。
把函数式组件转化为类组件
你可以遵从以下5步, 把一个类似 Clock
这样的函数式组件转化为类组件:
-
创建一个继承自
React.Component
类的 ES6 class 同名类。 -
添加一个名为
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>
);
}
}
Clock
现在被定为类组件,而不是函数式组件。
每次更新发生时都会调用 render
方法,但只要我们将 <Clock />
渲染到同一个 DOM 节点中,就只会使用 Clock
类的单例。 这让我们可以使用额外功能,例如本地状态(state) 和 生命周期钩子。
在类组件中添加本地状态(state)
我们现在通过以下3步, 把date
从属性(props
) 改为 状态(state
):
- 替换
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>
);
}
}
- 添加一个 类构造函数(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(props) {
super(props);
this.state = {date: new Date()};
}
类组件应始终使用 props
调用基础构造函数。
- 移除
<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
设置自己的计时器,并每秒更新一次。
在类中添加生命周期方法
在一个具有许多组件的应用程序中,在组件被销毁时释放所占用的资源是非常重要的。
当 Clock
第一次渲染到DOM时,我们要设置一个定时器 。 这在 React 中称为 “挂载(mounting)” 。
当 Clock
产生的 DOM 被销毁时,我们也想清除该计时器。 这在 React 中称为 “卸载(unmounting)” 。
当组件挂载和卸载时,我们可以在组件类上声明特殊的方法来运行一些代码:
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
具有特殊的含义,如果您需要存储不参与数据流的内容(如计时器 ID ),则可以自由向该类手动添加其他字段。
我们在 componentWillUnmount()
生命周期钩子中取消这个计时器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们将实现一个名为 tick()
的方法,以便 Clock
组件每秒运行一次。
它将使用 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
。我们稍后会更新此状态。 -
然后 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,
});
要解决这个问题,应该使用第 2 种 setState()
的格式,它接收一个函数,而不是一个对象。该函数接收前一个状态值作为第 1 个参数, 并将更新后的值作为第 2 个参数:
// 正确
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
我们在上面使用了一个箭头函数,但是也可以使用一个常规的函数:
// 正确
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
state(状态)更新会被合并
当你调用 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
的值。
数据向下流动
无论作为父组件还是子组件,它都无法获悉一个组件是否有状态,同时也不需要关心另一个组件是定义为函数组件还是类组件。
这就是 state(状态) 经常被称为 本地状态 或 封装状态的原因。 它不能被拥有并设置它的组件 以外的任何组件访问。
一个组件可以选择将 state(状态) 向下传递,作为其子组件的 props(属性):
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
同样适用于用户定义组件:
<FormattedDate date={this.state.date} />
FormattedDate
组件通过 props(属性) 接收了 date
的值,但它仍然不能获知该值是来自于 Clock
的 state(状态) ,还是 Clock
的 props(属性),或者是直接手动创建的:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
这通常称为一个“从上到下”,或者“单向”的数据流。任何 state(状态) 始终由某个特定组件所有,并且从该 state(状态) 导出的任何数据 或 UI 只能影响树中 “下方” 的组件。
如果把组件树想像为 props(属性) 的瀑布,所有组件的 state(状态) 就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。
要证明所有组件都是完全独立的, 我们可以创建一个 App
组件,并在其中渲染 3 个 <Clocks>
:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个 Clock
都设置它自己的计时器并独立更新。
在 React 应用中,一个组件是否是有状态或者无状态的,被认为是组件的一个实现细节,随着时间推移可能发生改变。你可以在有状态的组件中使用无状态组件,反之亦然。