此文章是翻译State and Lifecycle这篇React(版本v16.2.0)官方文档。

State and Lifecycle

考虑之前章节中的时钟例子。

到现在为止,我们只会通过一种方式进行更新UI。

我们通过调用ReactDOM.render() 方法去改变渲染输出:

  1. function tick() {
  2. const element = (
  3. <div>
  4. <h1>Hello World!</h1>
  5. <h2>It is {new Date().toLocaleTimeString()}.</h2>
  6. </div>
  7. );
  8. ReactDOM.render(
  9. element,
  10. document.getElementById('root')
  11. )
  12. }
  13. setInterval(tick, 1000)

在CodePen 上尝试

在这一章节中,我们将学习如何使Clock 组件真正可复用和封装。它将设置自己的定时器来每一秒更新它自己。

我们开始封装始时钟外观:

  1. function Clock(props) {
  2. return (
  3. <div>
  4. <h1>Hello World!</h1>
  5. <h2>It is {props.date.toLocaleTimeString()}.</h2>
  6. </div>
  7. );
  8. }
  9. function tick(){
  10. ReactDOM.render(
  11. <Clock date={new Date()} />,
  12. document.getElementById('root')
  13. )
  14. }
  15. setInterval(tick, 1000)

在CodePen 上尝试

但是,它忽略了一个关键的需求:一个事实,即Clock 设置一个定时器来每隔一秒进行更新UI ,应该是Clock 的实现细节。

理想状况下,我们只写一次来让Clock 更新自身:

  1. ReactDOM.render(
  2. <Clock date={new Date()} />,
  3. document.getElementById('root')
  4. )

为了实现,我们需要在Clock 组件中添加“state”。

State 同props 非常相似,但是它是私有的完全被组件控制。

我们之前提到过 组件使用类定义有额外的特性(additional features)。本地State 就是这样:只能通过类提供此功能。

Converting a Function to a Class

你可以通过五步来实现将一个函数式组件类似Clock 转成类:

  1. 使用相同的名字实现一个ES6 class 并继承React.Component
  2. 添加一个render() 空方法。
  3. 将函数体内容移到render() 方法内。
  4. render() 方法体内将props 替换为this.props
  5. 删除空的函数声明。
  1. class Clock extends React.Component {
  2. render() {
  3. return (
  4. <div>
  5. <h1>Hello World!</h1>
  6. <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
  7. </div>
  8. );
  9. }
  10. }

在CodePen 上尝试

Clock 现在是通过类定义而不是组件。

现在我们可以使用额外的特性例如本地state 和生命周期钩子。

Adding Local State to a Class

通过三步将date 从props 移到state: 1)在render() 方法中 将this.props.date 替换为 this.state.date

  1. class Clock extends React.Component {
  2. render() {
  3. return (
  4. <div>
  5. <h1>Hello World!</h1>
  6. <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
  7. </div>
  8. );
  9. }
  10. }

2)添加一个class constructor 并给this.state 赋初值:

  1. class Clock extends React.Component {
  2. constructor(props){
  3. super(props);
  4. this.state = {date: new Date()};
  5. }
  6. render() {
  7. return (
  8. <div>
  9. <h1>Hello World!</h1>
  10. <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
  11. </div>
  12. );
  13. }
  14. }

注意,我们将props 传入了基本的构造函数中:

  1. constructor(props){
  2. super(props);
  3. this.state = {date: new Date()};
  4. }

类组件应该总是使用props 调用基本的构造函数。

3)将date<Clock /> 元素中移除:

  1. ReactDOM.render(
  2. <Clock />,
  3. document.getElementById('root')
  4. );

稍后我们将组件自身添加定时器。

结果看上去如下:

  1. class Clock extends React.Component {
  2. constructor(props){
  3. super(props);
  4. this.state = {date: new Date()};
  5. }
  6. render() {
  7. return (
  8. <div>
  9. <h1>Hello World!</h1>
  10. <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
  11. </div>
  12. );
  13. }
  14. }
  15. ReactDOM.render(
  16. <Clock />,
  17. document.getElementById('root')
  18. );

在CodePen 上尝试

接下来,我们将给Clock 设置定时器并每秒更新一次。

Adding Lifecycle Methods to a Class

在一个使用许多组件的应用中,当组件被销毁时去释放资源是非常重要的。

无论什么时候当Clock 第一次渲染到DOM 时,我们要设置一个定时器。在React 中被称为“挂载(mounting)”。

无论什么时候当Clock 产生的DOM 被移除时去,我们也要清除定时器。在React 中被称为“卸载(unmounting)”。

当一个组件被挂载和卸载时,我们将在通过在类组件中声明特殊的方法(special methods)去运行一些代码:

  1. class Clock extends React.Component {
  2. constructor(props){
  3. super(props);
  4. this.state = {date: new Date()};
  5. }
  6. componentDidMount() {
  7. }
  8. componentWillUnmount() {
  9. }
  10. render() {
  11. return (
  12. <div>
  13. <h1>Hello World!</h1>
  14. <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
  15. </div>
  16. );
  17. }
  18. }

这些方法被称为“(生命周期钩子)lifecyle hooks”。 这个componentDidMount() 钩子是在组件的输出已经被渲染到DOM 中之后运行。这是一个好地方去设置一个定时器:

  1. componentDidMount() {
  2. this.timerID = setInterval(
  3. () => this.tick(),
  4. 1000
  5. );
  6. }

注意我们将定时器ID 保存在this 上。

虽然this.props 是被React 自身设置的的但是this.state 有特殊的含义,如果你要存储一些不用于可视化输出的东西,你可以手动自由添加额外的字段到类上。

如果你不需要在render() 中使用一些东西,它不应该存储在state 中。

我们会在componentWillUnmount() 生命周期钩子中销毁这个定时器:

  1. componentWillUnmount() {
  2. clearInterval(this.timerID);
  3. }

最后,我们将实现这个tick() 方法,Clock 组件每秒运行一次。

它将会使用this.setState() 去调度更新组件的本地 state:

  1. class Clock extends React.Component {
  2. constructor(props ){
  3. super(props);
  4. this.state = {date: new Date()};
  5. }
  6. componentDidMount() {
  7. this.timerID = setInterval(
  8. () => this.tick(),
  9. 1000
  10. );
  11. }
  12. componentWillUnmount() {
  13. clearInterval(this.timerID);
  14. }
  15. tick() {
  16. this.setState({
  17. date: new Date()
  18. });
  19. }
  20. render() {
  21. return (
  22. <div>
  23. <h1>Hello World!</h1>
  24. <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
  25. </div>
  26. );
  27. }
  28. }
  29. ReactDOM.render(
  30. <Clock />,
  31. document.getElementById('root')
  32. );

在CodePen 上尝试

现在这个时钟每秒滴答一次。

让我们快速回顾一下每一步都调用那个方法:

1)当将<Clock /> 传给ReactDOM.render() 方法时,React 调用 Clock 组件的构造器方法。由于Clock 需要展示当前时间,它使用一个包含当前时间的对象初始化this.state。我们之后将更新这个state。

2)React 然后调用Clock 组件的render() 方法。这是React 了解要在屏幕上展示什么。React 然后更新DOM 去匹配Clock 的渲染输出。

3)当Clock 输出被插入到DOM 中,React 调用componentDidMount() 生命周期钩子。在它内部,Clock 组件要求浏览器设置一个定时器去每隔一秒调用tick()

4)每隔一秒浏览器就调用tick()方法。在它内部,Clock 组件调度一个UI 更新通过调用一个包含当前时间对象的setState() 方法。由于setState() 调用,React 知道当前state 已经发生变化,于是又去调用render() 方法去了解屏幕上应该显示什么。这次,this.state.daterender() 中是不同的,所以渲染输出会包含当前时间。React 相应地更新DOM。

5)如果Clock 组件被从DOM 中移除,React 将会调用componentWillUnmount() 生命周期钩子所以这个定时器会停止。

Using State Correctly

你需要知道关于setState() 的三件事。

Do Not Modify State Directly

例如,它将不会重新渲染组件:

  1. // Wrong
  2. this.state.comment = 'Hello';

相反,应该使用setState()

  1. // Correct
  2. this.setState({comment: 'Hello'});

唯一能够直接赋值this.state的是在构造器中。

State Updates May Be Asynchronous

为了性能React 可能批处理多个setState() 调用在一次更新中。

因为this.propsthis.state 可以异步更新,你不应该依赖它们中任意值去计算下一个state。

例如,下面代码更新计数器会可能会失败:

  1. // Wrong
  2. this.setState({
  3. counter: this.state.counter + this.props.increment
  4. })

为了解决这个问题,使用另一种方式的setState() 接受一个函数而不是一个对象。这个函数可以接受之前的state 作为第一个参数,此时props 可以被应用为第二个参数:

  1. // Correct
  2. this.setState((prevState, props) => ({
  3. counter: prevState.counter + props.increment
  4. }));

在上面我们使用了arrow function,但是我们使用常规函数也是可以工作的:

  1. // Correct
  2. this.setState(function(prevState, props) {
  3. return {
  4. counter: prevState.counter + props.increment
  5. };
  6. });

State Updates are Merged

当你调用setState(),React 会合并你提供的对象到当前state 中。

例如,你的state 可能包含几个独立的变量:

  1. constructor(props) {
  2. super(props);
  3. this.state = {
  4. posts: [],
  5. comments: []
  6. };
  7. }

然后你可以独立地调用几个setState()方法:

  1. componentDidMount() {
  2. fetchPosts().then(response => {
  3. this.setState({
  4. posts: response.posts
  5. });
  6. });
  7. fetchComments().then(response => {
  8. this.setState({
  9. comments: response.comments
  10. });
  11. });
  12. }

合并操作是隐式的,所以this.setState({comments}) 保留了this.state.post 完整,但是完全替换了this.state.comments

The Data Flows Down

父组件或子组件都不知道某一个组件是有状态的(stateful ) 还是无状态的(stateless ),并且它们也不管它是被定义为一个函数还是一个类。

这就是为什么state 经常被称为本地的(local)或者是封装的(encapsulated)。它不能被除拥有它之外的组件进行设置。

一个组件可以将它的state 作为子组件的props 进行传递:

  1. <h2>It is {this.state.date.toLocaleTimeString()}.</h2>

在用户自定义组件中工作:

  1. <FormattedDate date={this.state.date} />

这个FormattedDate 组件在它的props 中接受date 而不需要知道它是来自Clock 的state ,还是props,亦或是手动输入的:

  1. function FormattedDate(props) {
  2. return <h2>It is {props.date.toLocaleTimeString()}.</h2>
  3. }

在CodePen 上尝试

这就是自上而下(top-down)或单向的(unidirectional)数据流。任何state 都是有某些特定component 拥有的,任何来自该state 的数据或UI 都只能影响位于层级树之下的组件。

如果你设想一个组件树是一个props 的瀑布流,每一个组件的state 都像是一个在任意位置加入的额外的只能向下流的水源。

为了展示所有的组件都是独立的,我们可以创建一个App 组件去渲染三个Clock

  1. function App() {
  2. return (
  3. <div>
  4. <Clock />
  5. <Clock />
  6. <Clock />
  7. </div>
  8. );
  9. }
  10. ReactDOM.render(
  11. <App />,
  12. document.getElementById('root')
  13. );

在CodePen 上尝试

每一个Clock 都独立地设置它们自己的定时器进行更新。

在React 应用中,组件是否有状态是一个可以随着时间变化的实现细节。你可以在有状态的组件中使用无状态的组件,反之亦然。