State & 生命周期
本例子将封装可复用的 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 类似,但是 state 是私有的,并且完全受控于当前组件。
将函数组件转换成 class 组件
通过以下五步将 Clock
的函数组件转成 class 组件:
- 创建一个同名的ES6 Class,并且继承于
React.Component
。 - 添加一个空的
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>
);
}
}
每次组件更新时 render
方法都会被调用,但只要在相同的 DOM 节点中渲染 <Clock />
,就仅有一个 Clock
组件的 class 实例被创建使用。
向 class 组件中添加局部的 state
通过以下三步将 date
从 props 移动到 state 中:
1.把
render()方法中的
this.props.date替换成
this.state.date :
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
2.添加class构造函数,在函数中为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()};
}
Class 组件应该始终使用 props
参数来调用父类的构造函数。
3.移除 <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')
);
将生命周期方法添加到 Class 中
当 Clock
组件第一次被渲染到 DOM 中的时候,就为其设置一个定时器。这在 React 中被称为“挂载(mount)”。
同时,当 DOM 中 Clock
组件被删除的时候,应该清除定时器。这在 React 中被称为“卸载(unmount)”。
为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法:
componentDidMount() { }
componentWillUnmount() { }
这些方法叫做“生命周期方法”。
componentDidMount()
方法会在组件已经被渲染到 DOM 中后运行,所以,最好在这里设置计时器:
componentDidMount() {
this.timerID = setInterval( () => this.tick(), 1000 ); }
接下来把计时器的 ID 保存在 this
之中(this.timerID
)。
我们会在 componentWillUnmount()
生命周期方法中清除计时器:
componentWillUnmount() {
clearInterval(this.timerID); }
最后,我们会实现一个叫 tick()
的方法,Clock
组件每秒都会调用它。
使用 this.setState()
来时刻更新组件 state:手动调用更新
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 />
被传给ReactDOM.render()
的时候,React 会调用Clock
组件的构造函数。因为Clock
需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化this.state
。我们会在之后更新 state。 - 之后 React 会调用组件的
render()
方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配Clock
渲染的输出。 - 当
Clock
的输出被插入到 DOM 中后,React 就会调用ComponentDidMount()
生命周期方法。在这个方法中,Clock
组件向浏览器请求设置一个计时器来每秒调用一次组件的tick()
方法。 - 浏览器每秒都会调用一次
tick()
方法。 在这方法之中,Clock
组件会通过调用setState()
来计划进行一次 UI 更新。得益于setState()
的调用,React 能够知道 state 已经改变了,然后会重新调用render()
方法来确定页面上该显示什么。这一次,render()
方法中的this.state.date
就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。 - 一旦
Clock
组件从 DOM 中被移除,React 就会调用componentWillUnmount()
生命周期方法,这样计时器就停止了。
正确地使用 State
不要直接修改 State
// Wrong
this.state.comment = 'Hello';
而是应该使用 setState()
:
// Correct
this.setState({comment: 'Hello'});
构造函数是唯一可以给 this.state
赋值的地方:
State 的更新可能是异步的
出于性能考虑,React 可能会把多个 setState()
调用合并成一个调用。
可以让 setState()
接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
上面使用了 箭头函数 ,不过使用普通的函数也同样可以:
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
State 的更新会被合并
当你调用 setState()
的时候,React 会把你提供的对象合并到当前的 state。
例如,你的 state 包含几个独立的变量:
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
。
数据是向下流动的
不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。
这就是为什么称 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 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。
为了证明每个组件都是真正独立的,我们可以创建一个渲染三个 Clock
的 App
组件:
function App() {
return (
<div>
<Clock /> <Clock /> <Clock /> </div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个 Clock
组件都会单独设置它自己的计时器并且更新它。