【译】快速起步-状态和生命周期
version: 15.4.2
状态和生命周期
到目前为止,我们仅学到了一种更新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
:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
想要实现这个目标,我们需要给 Clock
组件添加 state
。
state
和 props
类似,但它是组件内部管理的的私有变量。
我们 之前提到,当使用 Class
定义组件时,可以使用一些附加的特性。本地状态就是这样的特性,仅在用 Class
定义组件时可用。
将 Function
转换为 Class
使用以下五步,可以将组件 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
组件是用类定义的,而不是函数定义的。
这让我们可以使用一些附加特性,如本地状态和生命周期钩子。
添加本地状态到类
通过以下三个步骤,我们将会把 date
从 props
移动到 state
中:
- 在
render()
方法中,使用this.state.date
来替代this.props.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
。
在组件的 mounting
和 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>
);
}
}
这些方法就被称之为生命周期钩子(lifecycle hooks)。
componentDidMount()
钩子在组件渲染到DOM之后运行,这是启动定时器的好地方:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意我们是如何在 this
上保存定时器ID的
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
会每秒跳动。
我们来简单回顾下发生了什么以及调用方法的顺序:
-
当
<Clock />
传递给ReactDOM.render()
时,React
调用了Clock
组件的构造函数。由于Clock
需要显示当前时间,我们通过一个包含当前时间的对象来初始化this.state
。稍后,我们将更新这个state
。 -
接下来,
React
调用Clock
组件的render()
方法。这决定了React
将会在屏幕上显示什么。接着,React 将组件的输出更新到DOM上。 -
当
Clock
组件被添加到DOM之后,React会调用componentDidMount()
钩子函数。 在它的内部,Clock
组件启动浏览器定时器,并每秒执行一次tick()
。 -
每一秒,浏览器都会调用
tick()
方法。在它的内部,Clock
组件将包含当前时间的对象传递给this.setState()
,并以此来调度UI变更。当调用setState()
方法,React
知道了state
变化,然后就调用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()
的第二种形式,接受一个函数。这个函数的第一个参数是上一个状态,第二个参数是当前属性:
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
以上代码我们使用了 arrow function ,但常规函数也是可以正常使用的:
// 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
。
数据流向
父组件和子组件都不必要知道某个组件是有状态的还是无状态的,并且它们不应该关心它是否被定义函数或类。
这也是为什么 state
通常被称为本地状态或封装状态。因为除了组件本身之外,其他组件都是不能直接访问组件状态的。
父组件可以选择将状态通过子组件的属性进行传递:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
This also works for user-defined components:
<FormattedDate date={this.state.date} />
FormattedDate
组件将通过它自身的 props
接收到 date
,不需要知道 date
是来自哪里(Clock
组件的状态或属性,乃至手动输入):
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
这通常被称作 从上到下
或 单向
数据流。任何 state
始终由某个特定组件拥有,并且从该 state
导出的任何数据或UI都只会影响子集。
如果把组件树想象为瀑布,每个组件的状态都是一个额外的水源,它可以从任一点汇入,但都向下流动。
为了表明组件都是相互隔离的,我们可以创建一个包含三个 Clock
组件的 App
组件:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个 React
有自己的定时器,并独立更新(并不会相互影响)。
在 React
应用中,组件有无状态被认为是可变的,它是组件的具体实现细节。你可以在有状态组件中使用无状态组件,反之亦然。