state 和 生命周期 到目前为止,我们仅仅学习了一种方式来更新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非常相似,但是它是私有的,而且完全被组件自身控制
我们前面提到过,以类方式,定义的组件,有一些额外的特性。
局部state就是类可用的功能之一
将函数式组件,转换成类组件
你可以传唤一个函数式组件,例如 Clock组件,到一个类组件,通过5个步骤:
1.创建一个同名的ES6类,继承 React.Component
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>
);
}
}
此时,Clock组件被定义为一个类,而不是函数
这就让我们可以使用 '附加特性',例如:局部state和生命周期钩子
向类中添加局部state
我们通过3步,将props中的data移动到state:
1.在render()方法中,使用 'this.state.data' 替换 'this.props.date'
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> // 这行
</div>
);
}
}
2.添加类的构造器,分配初始的 '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)
constructor(props) {
super(props);
this.state = {date: new Date()};
}
组件类,应该总是传入 'props' 调用基础构造器
3.从<Clock />元素上,移除 'date' prop
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组件首次渲染为dom时,我们设置了一个计时器(set up a timer)。这在React中,被称作 'mounting'
当移除由clock组件产生的dom时,我们清除mount设置的计时器。这在React中,被称作 'unmouting'
我们可以在组件类中,声明一些特殊的方法,当组件 mount 和 unmount 时,来运行我们设置的代码:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {data: new Date()};
}
componentDitMount(){
}
componentWillUnmount(){
}
render() {
return (
<h1>It is {this.state.date.toLocaleTimeString()}.</h1>
);
}
}
这些方法被称作 '生命周期 钩子'
组件输出已经被渲染为dom后,运行 'componentDitMount()' 钩子。这是个设置计时器的好地方:
componentDitMount(){
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意我们是如何在 'this' 对象上,保存 计时器ID的。
this.props由React自身创建,而this.state有特殊的意义,如果你需要存储一些数据,且这些数据并不用于可见输出(the visual output),你可以手动给组件类添加额外的字段
如果你在 render() 中不使用其他字段,则不应该在 state 中添加这些字段
我们将在 'componentWillunmount' 生命周期钩子中,清除计时器
componentWillunmount() {
clearInterval(this.timerID);
}
最后,我们将实现 tick() 方法,将会每秒运行一次
tick() 方法,使用 'this.setState()' 来更新组件的私有state
tick() {
this.setState({
data: new Date();
});
}
正确使用 state
关于setState(),你应该知道3件事:
1.不要直接修改state
例如,下面的代码,将不会重新渲染组件:
this.state.comment = 'Hello'; // 错误
使用 setState() 来代替:
this.setState({comment: 'Hello'}); // 正确
2.state的更新,可能异步的
为了性能考虑,React可以在一次更新中,批量执行 setSate() 调用
因为 this.props 和 this.state 可能被异步更新,在计算接下来的state,你不应该依赖它们的值(可能还未被更新)
例如,下面的更新counter的代码可能失败:
this.setState({
counter: this.state.counter + this.props.increment; // 错误
});
为了修复上面的错误,使用setState()的第二种形式,接收一个函数(function),而非一个对象(object)。函数接收上一次的state作为第一个参数,更新应用时的props作为第二个参数:
this.setState((prevState, props) => ({
counter : prevState.counter + props.increment // 正确
}));
上面,我们使用了一个 '箭头函数'(arrow function),也可以使用一般的函数:
this.setState(function(prevState, props) {
return {
counter : prevState.counter + props.increment // 正确
}
});
3.state更新会被合并
当调用 'setState()'时,React会合并 你提供的对象(the object you provide) 到当前的state。
例如:提供的state可能包含几个独立的变量:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
接着,可以使用独立的 setState() 调用来分别更新它们:
componentDitMount() {
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
数据流向下传递(the data flows down)
父组件和子组件,都不知道某个组件是 'stateful' 还是 'stateless',而且,它们不关心它们被定义为一个函数还是类。
这就是为什么state经常被称作 局部(local) 或者 包装、封装(encapsulated)。对于除了拥有并设置它的组件之外的任意其他组件,它是不可访问的。
同 props 一样,组件也可以选择,将它的state传递给它的子组件:
<h2>It is {this.state.date.loLocaleTimeString()}.</h2>
对用户自定义组件也是有效的:
<FormattedDate data={this.state.date} />
FormattedDate 组件接收props中的date,但是并不知道它是来自'clock' 的state,props,还是手动输入:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
这通常被称为“自上而下”或“单向”的数据流。任何由某些特定组件拥有的state,任何来自该state的数据或UI,仅仅会影响树形结构中,它们的下级组件.
想象一个组件树是一个props瀑布,每个组件的state就像一个附加的水源,在任意点加入到瀑布中,但是也向下流动。
为了显示所有的组件是完全隔离的,我们可以创建一个app组件,渲染3个 clock 组件:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个Clock设置它自己的计时器,并且独立的更新。
在React应用中,不论组件是有状态的(stateful),还是无状态的(stateless),都被认为是组件的实现细节,可以随着时间而改变。你可以在有状态的组件中使用无状态组件,反过来也一样。