在上一篇文章:React.js的基础知识及一些demo(一)中,我们介绍了React.js的元素、JSX语法、组件和属性等相关基础语法及一些简单demo。这篇文章我们继续往下了解React的语法。
状态和生命周期
在上一篇文章更新已渲染的元素一节中,有一个时钟的例子。
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);
在时钟例子中,我们通过调 ReactDOM.render() 方法来更新渲染的输出,这是一种更新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 自身实现的一部分。
理想情况下,我们应该只引用一个 Clock , 然后让它自动计时并更新:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
要实现这点,我们需要添加 state 到 Clock 组件。state 和 props 类似,但是它是私有的,并且由组件本身完全控制。
在上一篇文章中提到,组件有两种定义方式:类组件和函数组件。用类定义的组件有一些额外的特性。 这个”类专有的特性”, 指的就是局部状态。
如何将函数式组件转换为类组件
在上一小节中,我们定义的Clock组件属于函数式组件,我们以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 现在被定为类组件,而不是函数式组件。类允许我们在其中添加本地状态(state)和生命周期钩子。
在类组件中添加本地状态(state)
我们现在通过以下3步, 把date从属性(props) 改为 状态(state):
1.替换 render() 方法中的 this.props.date 为 this.state.date;
2.添加一个 类构造函数(class constructor) 初始化 this.state
;
3.移除 <Clock /> 元素中的 date 属性;
结果如下所示:
class Clock extends React.Component{
constructor(props){
super(props);//调用父类的constructor(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')
);
这里:super关键字代表父类的实例(即父类的this对象)。
注意我们如何将 props 传递给基础构造函数,类组件应始终使用 props 调用基础构造函数。
接下来,我们将使 Clock 设置自己的计时器,并每秒更新一次。
在类中添加生命周期方法
在一个具有许多组件的应用程序中,在组件被销毁时释放所占用的资源是非常重要的。
当 Clock
第一次渲染到DOM时,我们要设置一个定时器 。 这在 React 中称为 “挂载(mounting)” 。
当 Clock
产生的 DOM 被销毁时,我们也想清除该计时器。 这在 React 中称为 “卸载(unmounting)” 。
当组件挂载和卸载时,我们可以在组件类上声明特殊的方法来运行一些代码,这些方法称为 “生命周期钩子”。
1.componentDidMount() 钩子在组件输出被渲染到 DOM 之后运行。这是设置时钟的合适位置;
- 注意我们把计时器ID直接存在 this 中。
- this.props 由 React 本身设定, 而 this.state 具有特殊的含义,但如果需要存储一些不用于视觉输出的内容,则可以手动向类中添加额外的字段。
- 如果在 render() 方法中没有被引用, 它不应该出现在 state 中。
2.我们在componentWillUnmount()生命周期钩子中取消这个计时器;
componentDidMount(){
this.timerID = setInterval(() => this.tick(),1000);
}
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')
);
关于这个例子的总结
我们来快速回顾一下该过程,以及调用方法的顺序:
1.当 <Clock /> 被传入 ReactDOM.render() 时, React 会调用 Clock组件的构造函数。 因为 Clock 要显示的是当前时间,所以它将使用包含当前时间的对象来初始化 this.state 。我们稍后会更新此状态。
2.然后 React 调用了 Clock 组件的 render() 方法。 React 从该方法返回内容中得到要显示在屏幕上的内容。然后,React 更新 DOM 以匹配 Clock 的渲染输出。
3.当 Clock 输出被插入到 DOM 中时,React 调用 componentDidMount() 生命周期钩子。在该方法中,Clock 组件请求浏览器设置一个定时器来一次调用 tick()。
4.浏览器会每隔一秒调用一次 tick()方法。在该方法中, Clock 组件通过 setState() 方法并传递一个包含当前时间的对象来安排一个 UI 的更新。通过 setState(), React 得知了组件 state(状态)的变化, 随即再次调用 render() 方法,获取了当前应该显示的内容。 这次,render() 方法中的 this.state.date 的值已经发生了改变, 从而,其输出的内容也随之改变。React 于是据此对 DOM 进行更新。
5.如果通过其他操作将 Clock 组件从 DOM 中移除了, React 会调用 componentWillUnmount() 生命周期钩子, 所以计时器也会被停止。
使用 State(状态)的一些注意点
关于 setState() 有三件事是你应该知道的。
1.不要直接修改 state(状态)
例如,这样将不会重新渲染一个组件:
// 错误
this.state.comment = 'Hello';
应该使用 setState() 代替:
// 正确
this.setState({comment: 'Hello'});
唯一可以分配 this.state 的地方是构造函数。
2.state(状态) 更新可能是异步的
React 为了优化性能,有可能会将多个 setState() 调用合并为一次更新。
因为 this.props 和 this.state 可能是异步更新的,你不能依赖他们的值计算下一个state(状态)。
例如, 以下代码可能导致 counter(计数器)更新失败:
// 错误
this.setState({
counter: this.state.counter + this.props.increment,
});
要解决这个问题,应该使用另一种 setState() 的形式,它接受一个函数而不是一个对象。这个函数将接收前一个状态作为第一个参数,应用更新时的 props 作为第二个参数:
// 正确
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
3.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 应用中,一个组件是否是有状态或者无状态的,被认为是组件的一个实现细节,随着时间推移可能发生改变。你可以在有状态的组件中使用无状态组件,反之亦然。
参考: