React版本:15.4.2
**翻译:xiyoki **
考虑前一节中滴答作响的时钟的例子。
到目前为止,我们只学习了一种更新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是组件私有的,并且完全由组件控制。
我们之前提到了定义为类的组件有一些额外的功能。
Local State是指:一个只对类组件有效的功能。
Converting a Function to a Class(将一个函数转换为类)
你只需5步就能将一个** functional component ** 转换成一个 ** class component ** :
- 用相同的名字创建一个ES6类,并且继承
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>
);
}
}
Clock现在被定义成了一个类而不是函数。
这使我们可以使用额外的功能,如局部状态和生命周期钩子。
Adding Local State to a Class(将本地状态添加到类)
只需三步,我们就将date从props移动到state:
1)在render()
方法中用 this.state.date
替换 this.props.date
:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
- 添加一个 class constructor,用来指定
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到base constructor的:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
类组件在调用base constructor时应当总是传递props。
- 从
<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')
);
下面,我们将使Clock
设置它自己的计时器,并且每秒钟更新自己。
Adding Lifecycle Methods to a Class(向类添加生命周期方法)
在具有许多组件的应用程序中,当组件被销毁时释放组件所占用的资源非常重要。
每当Clock被首次渲染到DOM时,我们都要建立一个定时器。这在React中被称作 "mounting(安装)"。
每当由Clock产生的DOM被移除时,我们也希望清除定时器。这在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>
);
}
}
这些方法被称为‘lifecycle hooks(生命周期钩子)。
在组件的输出(组件返回的React元素)已被渲染到DOM上后,componentDidMount()
hook 运行。这是设置计时器的好地方:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意我们是如何保存timerID到this上的。
虽然this.props
由React元素自身设置,this.state
也有其特殊的意义,然而如果你需要存储东西不用于可视化输出,那么你也可以自由地手动添加其他字段到类。
如果一些数据你没有在render()
中使用,那么这些数据就不应该出现在state中。
我们将在componentWillUnmount()
生命周期钩子中清除计时器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们将要实现每秒运行一次的tick()
方法。
它将使用this.setState()
去排定组件local 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
组件的constructor
。 由于Clock
需要展示当前的时间,它便用一个包含当前时间的对象初始化this.state
。我们稍后会更新this state
.然后 React 调用
Clock
组件的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()
生命周期钩子函数,因此定时器就被停止了。
Using State Correctly(正确使用State)
关于setState(),你应该知道三件事。
Do Not Modify State Directly(不要直接修改state)
例如:这将不会重新渲染组件:
// Wrong
this.state.comment = 'Hello';
相反,使用setState()代替:
// Correct
this.setState({comment: 'Hello'});
唯一你可以指派this.state
的地方是constructor
。
State Updates May Be Asynchronous (状态更新可能异步)
为了性能,React会将多个setState()
调用放置到单一的更新中。
由于this.props
和this.state
可以被异步更新,因此你不应该依赖它们的值来计算下一个状态。
例如,此代码可能无法更新计数器:
this.setState({
counter: this.state.counter + this.props.increment,
});
要修复它,使用setState()
的第二种形式,它接收一个函数,而不是一个对象。该函数将接收先前的state作为第一个参数,并将更新时的props作为第二个参数:
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
上面我们使用了一个箭头函数,但也可以使用常规函数:
// Correct
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
State Updates are Merged(状态更新被合并)
当你调用setState()
时,React将你提供的对象合并到当前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
。
The Data Flows Down(数据向下流动)
不论是父组件,还是子组件都不知道某个组件是stateful还是stateless,并且它们不应该关心该组件是否被定义为函数或类。
这就是为什么状态通常被称为局部或封装。组件的状态除了拥有和设置它的组件外,其它任何组件都不能访问它。
组件可以选择将其state当做props向下传递给其子组件:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
这也适用于用户定义的组件:
<FormattedDate date={this.state.date} />
FormattedDate
组件将在其props中接收 date ,并且不知道date是来自于Clock的state, 还是Clock的 props, 亦或是手动声明的:
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
这通常被称为‘自顶向下’或‘单向’数据流。任何state总是由一些特定组件所拥有,并且从该state派生的任何数据或UI只能影响树中位于它们下面的组件。
如果你将组件树想象成由props构成的瀑布,那么每个组件的state就像一个额外的水源,它在任意点连接瀑布,并且也向下流。
为了展示所有组件是真正孤立的,我们可以创建一个App组件,该组件渲染三个<Clock />
:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每个Clock独立地设置其自己的计时器,独立地更新。
在React应用程序中,组件是stateful还是stateless被视为可能随时间更改的组件的实现细节。你可以在stateful组件内使用stateless组件,反之亦然。