React main concepts(五)
建议配合官网文档 State and Lifecycle 进行阅读。
State and Lifecycle
这一节将会介绍state, lifecycle的概念,你可以在detailed component API reference找到更详细的讲解。
在之前的章节中,我们有一个有一个可以显示时间的元素,当时我们的更新方式为重新创建一个新元素,并调用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);
在这一节,我们将学习如何时将这个时间元素变得可重用,并且封装它。我们的封装如下所示:
// 时钟函数组件
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);
这种封装方式有一个至关重要的问题,即时钟的更新时间setInterval()
应该属于Clock
组件的内部细节,但上述代码并没有将其放在Clock
内部,为了实现该功能,我们向Clock
中引入state
。
state 和 props 十分类似,但 state 是私有的,并且完全由组件控制,即可以类可以修改自己的 state.
Converting a Function to a Class
在需要使用 state 时,一般的做法是使用类组件,因此我们先将函数组件改写为类组件:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
我们知道,无论是类组件还是上面封装的函数组件,对于 React 来说都是等价的,在这种情况下,每次更新时 React 都会自动的调用render()
函数,但只会渲染在同一个 DOM 上,并不会像之前的章节那样每次都创建一个新实例。
下面,我们使用类组件中的 state 进行更好的封装。
Adding Local State to a Class
对于一个可复用的时钟来说,刷新的时间间隔最好自己的属性,而不是参数,因此我们通过如下方式将 props 变成 state:
- 在类中添加构造函数,初始化其
state
:class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
/* render函数 */
} :hexoPostRenderEscape–>
> 构造函数constructor()
中super(props)
不能省略,该语句表示初始化父类。
2. 将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>
);
}
}
3. 将传递给
Clock
的 props 删除,我们可以得到如下代码:class Clock extends React.Component {
/* 构造函数,用来初始化 satate */
constructor(props) {
super(props);
this.state = {date: new Date()};
}
/* render函数,用来返回希望显示的 UI 描述 */
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div>
);
}
}
/* 将组件渲染到根节点上 */
ReactDOM.render(
/* 这里的 Clock 不再接收 date="new Date()" */
<Clock />, document.getElementById('root')
);
最后,我们将计时器添加到组件本身。
Adding Lifecycle Methods to a Class
对于组件来说,当被销毁时,释放资源是十分重要的。
我们希望当Clock
被创建时,开启一个计时器,这在 React 中被称为挂载 mounting
我们也希望当Clock
被销毁时,能结束这个定时器,这咋 React 中被称为卸载 unmounting
我们可以在类组件中编写特定的函数,去规定当组件在挂载和卸载的时候需要执行的代码:
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()
, componentWillUnmount()
就被称作生命周期方法lifecycle methods
.
componentDidMount()
会在组件完成 DOM 渲染后执行,因此在这里开启定时器时合理的:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
这里我们将定时器变量保存在
this.timerID
中,相较与 React 内置的this.props
,this.state
,只要是不参加数据流动的变量,我们都可以自由的添加在类组件中,就如this.timerID
一样。
我们将会在componentWillUnmount()
中销毁启动的定时器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们会实现在定时器中回调的tick()
的函数:
tick() {
this.setState({
date: new Date()
});
}
最终,Clock
被封装成:
// Clock 类组件
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);
}
// 定时器中的回调函数,用来更新 state 中的 date
tick() {
this.setState({
date: new Date()
});
}
// 渲染函数,用来输出希望在 UI 中呈现的内容
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
// 将 Clock 渲染到根节点上
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
现在,让我们从头梳理一下函数的调用顺序:
- 当
<Clock />
被传递到ReactDOM.render()
中, React 调用Clock
的构造函数新建一个Clock
实例,在构造函数constructor()
中初始化Clock
中的state
。 - React 调用
Clock
中的render()
函数,通过其输出(返回的元素),React 更新 DOM 以匹配我们预期的结果。 - 当
Clock
确实插入到 DOM 中,React 调用componentDidMount()
生命周期函数,在该函数里,我们开启了定时器,并将其命名为timerID
绑定在实例中,该定时器会以1000毫秒为间隔重复调用tick()
函数。 - 每次
tick()
函数被调用时,Clock
会根据setState()
函数对state
的更新,更新 UI,由于setState()
函数的调用,React 可以知道何时state
被修改了,因此 React 在state
被修改时会自动重新调用render()
函数。 - 如果
Clock
组件从 DOM 中移除,则在移除前会调用componentWillUnmount()
生命周期函数,在该函数里,定时器将被清除。
Using State Correctly
在使用state
时,需要注意以下三点:
Do Not Modify State Directly
由于React 是通过setState()
来感知何时state
被修改了,因此直接修改state
可能不会使得组件被重新渲染:
// Wrong, 没有通过 setState() 修改state,组件将不会重新渲染
this.state.comment = 'Hello';
因此正确的做法是使用setState()
进行 state
的修改:
// Correct
this.setState({comment: 'Hello'});
this.state =
的语法只能用在构造函数constructor()
中以初始化state
.
State Updates May Be Asynchronous
React 可能将多个setState
函数批量调用以提升性能,因此setState()
的更新可能是异步的,因此通过当前的state
来计算下一state
是不可靠的:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
为了避免这个问题,当新的state
需要依赖当前state
时,不使用对象而采用函数的方式可以避免这个问题:
this.setState(
// 这是仅存在返回语句的箭头函数的简写
// 等价于 function(state, props) { return {counter: state.counter + props.increment}}
// 实际上 setState 接收到的还是一个对象
(state, props)=> ({
counter: state.counter + props.increment
})
)
State Updates are Merged
当setState()
被调用时,React 会将你提供的对象与当前的state
对象进行合并,也就是说,只有你提供的k-v会被修改,未提供的不会发生改变:
The Data Flows Down
对于任意一个组件,其父组件和子组件以及其余任何组件都无法获取它的state
,因此state
是局部的、封装的。
但是一个组件可以选择向其子组件(向下)传递它的state
,即作为子组件的props
:
<FormattedDate date={this.state.date} />
FormattedDate
组件接收date
作为它的props
,它并不关心props
中的数据是否来自父组件的state
,它只需要通过props
使用即可:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
这种数据流通常被称为单向数据流或数据向下流动。
每个组件都是独立的,如果我们创建三个Clock
组件,它们对拥有各自的计时器,并独立的进行更新,互不影响。
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);