version: 15.4.2
状态和生命周期
到目前为止,我们仅学到了一种更新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
类似,但它是组件内部管理的的私有变量。
我们 之前提到,当使用 Class
定义组件时,可以使用一些附加的特性。本地状态就是这样的特性,仅在用 Class
定义组件时可用。
将 Function
转换为 Class
使用以下五步,可以将组件 Clock
从函数定义方式转换到类定义方式:
创建一个同名的继承自
React.Component
的 ES6 class。在类中添加空的
render()
方法。将函数体内的代码复制到
render()
方法中。在
render()
方法中使用this.props
来替代props
。删除多余的空函数定义。
class Clock extends React.Component {
render() {
return (
Hello, world!
It is {this.props.date.toLocaleTimeString()}.
);
}
}
CodePen地址
现在 Clock
组件是用类定义的,而不是函数定义的。
这让我们可以使用一些附加特性,如本地状态和生命周期钩子。
添加本地状态到类
通过以下三个步骤,我们将会把 date
从 props
移动到 state
中:
1) 在 render()
方法中,使用 this.state.date
来替代 this.props.date
:
class Clock extends React.Component {
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
2) 添加 class constructor 并初始化 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
并调用基类构造函数。
3) 从
元素中移除 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
中称之为 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 (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
这些方法就被称之为生命周期钩子(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 (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
CodePen地址
现在 Clock
会每秒跳动。
我们来简单回顾下发生了什么以及调用方法的顺序:
1) 当
传递给 ReactDOM.render()
时,React
调用了 Clock
组件的构造函数。由于 Clock
需要显示当前时间,我们通过一个包含当前时间的对象来初始化 this.state
。稍后,我们将更新这个 state
。
2) 接下来,React
调用 Clock
组件的 render()
方法。这决定了 React
将会在屏幕上显示什么。接着,React 将组件的输出更新到DOM上。
3) 当 Clock
组件被添加到DOM之后,React会调用 componentDidMount()
钩子函数。 在它的内部, Clock
组件启动浏览器定时器,并每秒执行一次 tick()
。
4) 每一秒,浏览器都会调用 tick()
方法。在它的内部,Clock
组件将包含当前时间的对象传递给 this.setState()
,并以此来调度UI变更。当调用 setState()
方法,React
知道了 state
变化,然后就调用 render()
来确定要在屏幕上渲染的内容。这个时候, render()
中的 this.state.date
将和之前不同,会产生包含当前时间的新的输出。于是,React则对应的更新DOM。
5) 当 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
通常被称为本地状态或封装状态。因为除了组件本身之外,其他组件都是不能直接访问组件状态的。
父组件可以选择将状态通过子组件的属性进行传递:
It is {this.state.date.toLocaleTimeString()}.
This also works for user-defined components:
FormattedDate
组件将通过它自身的 props
接收到 date
,不需要知道 date
是来自哪里(Clock
组件的状态或属性,乃至手动输入):
function FormattedDate(props) {
return It is {props.date.toLocaleTimeString()}.
;
}
CodePen地址
这通常被称作 从上到下
或 单向
数据流。任何 state
始终由某个特定组件拥有,并且从该 state
导出的任何数据或UI都只会影响子集。
如果把组件树想象为瀑布,每个组件的状态都是一个额外的水源,它可以从任一点汇入,但都向下流动。
为了表明组件都是相互隔离的,我们可以创建一个包含三个 Clock
组件的 App
组件:
function App() {
return (
);
}
ReactDOM.render(
,
document.getElementById('root')
);
CodePen地址
每个 React
有自己的定时器,并独立更新(并不会相互影响)。
在 React
应用中,组件有无状态被认为是可变的,它是组件的具体实现细节。你可以在有状态组件中使用无状态组件,反之亦然。