# 状态和生命周期
思考前面章节中提到的时钟的例子。
迄今我们只了解了一种更新 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);
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/gwoJZk?editors=0010)。
在本节中,我们将会了解如何使 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 中[打开查看它](http://codepen.io/gaearon/pen/dpdoYR?editors=0010)。
然而,它丢失了一个重要的需求:事实是, Clock 设置一个时钟并每秒更新 UI 应该是 Clock 的实现细节。
理想情况下,我们希望只编写一次,使 Clock 更新它自己:
~~~
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
~~~
要实现这点,我们需要添加 “state” 到 Clock 组件。
状态和 props 类似,但是它是私有的,并且被组件完全控制。
我们之前提到的,组件定义为类有一些额外的功能。就是局部状态:只有类组件可以用的特性。
## 转换功能组件为类组件
可以通过五部转换一个像 Clock 这样的功能组件为类组件:
1. 创建一个继承 React.Component 类的 ES6 同名类
2. 添加一个空方法名为 render()
3. 把函数体移动到 render() 方法
4. 在 render() 方法中使用 this.props 替代 props
5. 删除保留的空函数声明
~~~
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/zKRGpo?editors=0010)。
Clock 现在被定义为一个 类组件 而不是功能组件。
这使我们可以使用如局部状态和生命周期钩子的额外功能。
## 向一个类组件添加局部状态
在有许多组件的应用中,非常重要的一点是当组件被销毁的时候要释放它们使用的资源。
我们希望无论何时 Clock 被首次渲染到 DOM 时[设置一个时钟](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval)。这在 React 中被称为 “mounting”。
另外我们还希望当 Clock 生成的 DOM 被移除时[清除这个时钟](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval)。在 React 中这叫做“unmounting”。
我们可以在组件类上声明特定的方法,当组件 mounts 或者 unmouts 时运行一些代码。
~~~
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.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')
);
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/amqdNA?editors=0010)。
现在 Clock 的 tick() 将在每秒运行。
让我们快速回顾以下其中的过程,和方法被调用的顺序:
1. 当 `<Clock />` 被传递到 ReactDOM.render(), React 调用 Clock 组件的构造函数。由于 Clock 需要显示当前时间,它使用一个包含当前时间的对象初始化了 this.state。我们之后会更新这个状态。
2. React 之后会调用 Clock 组件的 render() 方法。这是 React 知道该显示什么到屏幕的原因。React 之后匹配 Clock 的 render 输出的内容来更新 DOM。
3. 当 Clock 输出被插入到 DOM, React 调用 componentDidMount() 生命周期钩子。其中,Clock 组件要求浏览器设置一个计时器来在每秒调用一次 tick() 。
4. 浏览器每秒都会调用 tick() 方法。在这里面, Clock 组件通过调用 setState() 并传递一个包含当前时间的对象来安排一个 UI 的更新。得益于 setState() 的调用,React 知道状态被改变了,然后再次调用 render() 方法来了解什么应该显示在屏幕中。这次,在render() 方法中的 this.state.date 将是不同的,所以 render 输出中会包含更新的时间。React 对 DOM 进行相应的更新。
5. 如果 Clock 组件被从 DOM 中移除,React 调用 componentWillUnmount() 生命周期钩子,所以计时器也会被停止。
## 正确的使用状态
关于 setState() 有三件事是你应该知道的。
### 1.不要直接修改 state
例如,这将导致不能重新渲染组件:
~~~
// 错误用法
this.state.comment = 'Hello';
~~~
而是使用 setState() 替代:
~~~
// 正确用法
this.setState({comment: 'Hello'});
~~~
赋值 this.state 只有一个正确的地点,就是 constructor 中。
### 2. 状态更新可能是异步的
React 可能为了改进性能而批次处理多个 setState() 到一次更新。
因为 this.props 和 this.state 可能是异步更新的,你不能依赖他们的值计算下一个状态。
例如,这段代码可能导致更新 counter 失败:
~~~
// 错误
this.setState({
counter: this.state.counter + this.props.increment,
});
~~~
要弥补这个问题,使用另一种 setState() 的形式,它接受一个函数而不是一个对象。这个函数将接收前一个状态作为第一个参数,应用更新时的 props 作为第二个参数:
~~~
// 正确
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
~~~
我们在上面使用了一个[箭头函数](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions),但是也可以使用一个常规的函数:
~~~
// 正确
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
~~~
### 3.状态更新会被合并
当你调用 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 经常称为局部或者封装的原因。它不能被除了拥有并设置它的另外的任何组件访问。
一个组件可以选择向下传递它的状态作为它的子组件的 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>;
}
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/zKRqNB?editors=0010)。
这通常称为一个“从上到下”或者“单向”的数据流。任何状态总是被某个特定的组件所有,任何被这个状态驱动的数据或者 UI 都只影响树中“下方”的组件。
如果你设想一个组件树作为一个瀑布式的 props,每个组件的状态都像一个额外的水源,然后在任意点汇入它,但是同样只能向下流。
要展示这个,所有组件都是完全独立的,我们一个 App 组件来渲染三个 `<Clock>`:
~~~
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/vXdGmd?editors=0010)。
每个 Clock 都设置它自己的计时器并独立更新。
在 React App 中,一个组件是否是有状态或者无状态的,被认为是组件的一个实现细节,随着时间推移可能发生改变。你可以在有状态的组件中使用无状态组件,反之亦然。