React教程 - 5. 状态和生命周期

本文译自React官方文档
全文翻译及相关代码,请参看我的Github

回顾之前学习的时钟的例子,元素用于描述我们想要在屏幕上看到的内容。
我们调用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);

在CodePen中尝试
在本部分,我们将学习如何使Clock组件变得可重用,它会建立自己的定时器并每秒自动进行更新。
我们可以先从封装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);

在CodePen中尝试
但是,这里漏了一个很重要的地方:”Clock建立一个定时器并每秒更新UI”这一行为事实上应该是由Clock自己实现的(而不是将new Date作为属性传入)。
理想情况下它应该是这样的:

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

为了完成这个目的,我们应该为Clock组件增加状态(state)
状态与属性(props)类似,但它是私有的,且完全由组件控制。
我们之前提到过,被定义为class的组件拥有额外的特性,本地状态就是其中之一-一个只有类组件拥有的特性。

1.将函数式组件转换转为类组件

我们可以通过5步将Clock组件从函数式转换为类组件:

  1. 创建一个继承自React.ComponentES6 class
  2. 为其增加一个叫做render()的方法
  3. 将函数式组件的内容移至render()方法中
  4. 在移动的过程中,将原来的props转为this.props
  5. 删除剩余的空函数声明
    class Clock extends React.Component {
    render() {
     return (
       <div>
         <h1>Hello, world!</h1>
         <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
       </div>
     );
    }
    }
    
    在CodePen中尝试
    Clock组件现在从函数式组件变成了类组件,这使我们能够使用诸如state和Lifecycle Hooks等额外属性。

2.为类组件增加本地状态

我们通过以下3个步骤将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>
     );
    }
    }
    
  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(props) {
       super(props);
       this.state = {date: new Date()};
     }
    

    类组件应该总是在构造函数中执行super(props)这一过程

  3. 删除<Clock />中的date属性:

    ReactDOM.render(
    <Clock />,
    document.getElementById('root')
    );
    

    我们会在稍后将定时器相关的代码增加至组件内部。
    目前替换后的结果如下:
    ```javascript
    class Clock extends React.Component {
    constructor(props) {
    super(props);
    this.state = {date: new Date()};
    }

    render() {
    return (

     <h1>Hello, world!</h1>
     <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
    


    );
    }
    }

ReactDOM.render(
,
document.getElementById(‘root’)
);

[在CodePen中尝试](http://codepen.io/gaearon/pen/KgQpJd?editors=0010)  
接下来,我们要为``Clock``组件添加定时器使其每秒自动更新。

# 3.为类组件增加生命周期方法
在有多个组件的应用中,组件销毁时及时释放其所占用的资源是非常有必要的。  
我们想要在``Clock``被渲染到DOM后立即建立一个定时器,这在React中是__mount__阶段。  
我们还想在当``Clock``创建的DOM被销毁时清除该定时器,这在React中是__unmounting__阶段。  
我们可以在组件类中定义一些特殊的方法使得组件在mount/unmount时执行一些操作:  
```javascript
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”
当一个组件被渲染至DOM时执行componentDidMount,这是一个设置定时器的好时机:

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

注意,我们在这里将定时器的ID保存在了this中。
this.props由React自己建立,this.state有着特殊的意义-当有某些不是用于视觉输出的数据需要存储时,我们可以将其保存在class中。
如果这个数据不是用于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')
);

在CodePen中尝试
现在这个时钟能够开始动起来了。
让我们快速回顾一下发生了什么,以及各方法的调用顺序:

  1. <Clock />传递给ReactDOM.render()时,React调用了<Clock />的构造函数。
    由于Clock要显示当前时间,其将this.state初始化为一个包含当前时间的对象。我们将在稍后更新该state。
  2. React之后调用Clock组件的render()方法,根据该方法在屏幕上绘制UI。
  3. Clock输出被插入DOM时,React调用componentDidMount Life Hook。在该hook中Clock组件通知浏览器建立一个定时器并每秒调用一次tick()
  4. 浏览器每秒调用一次tick()方法。在该方法中,Clock组件通过调用setState()安排一次UI更新。
    setState()的调用让React收到状态改变的通知,并再次执行render()方法了解如何在屏幕上绘制UI。这次,render()方法中的this.state.date发生了变化,因此渲染输出包含更新了的时间。React根据它来更新DOM。
  5. 如果<Clock />组件从DOM中移除了,React会调用componentWillUnmount Life Hook,定时器将会被停止。

4.正确使用状态(State)

关于setState(),我们需要知道三点
不要直接改变state
例如,这种操作无法重新渲染一个组件:

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

应该使用setState()

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

我们唯一能直接给this.state赋值的地方是在组件的构造函数中。

状态更新可能是异步的
从性能的角度考虑,React可能会在某次单独的更新中批量执行多个setState()
由于this.propsthis.state可能会异步更新,我们不应该依靠他们的值来计算下一步的状态。
例如,以下代码可能无法准确的更新counter:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

为避免这个问题,可以使用setState()的另一种形式-接受函数而不是对象。传递给setState的函数将之前的状态作为第一个参数,需增加的属性作为第二个参数:

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

我们在这里使用了箭头函数,这么写也是一样的:

// Correct
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});

状态更新是合并更新
调用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的内容。

5.数据流向下

父组件或子组件都无法知道某组件是否是有状态的,它们也不应该组件是函数式组件还是类组件。
这也是为什么state通常是被本地调用或封装-它只能被拥有并建立它的组件访问,其他组件均不能访问它。
组件可以将state作为props传递给子组件:

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

也可以这么做:

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

FormattedDate组件将date作为其属性,但并不知道这个date的来源(是来自Clock的状态、Clock的属性或手动设置的):

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

在CodePen中尝试
这通常被称为“自顶向下”或“单向”数据量。任何state都是由某个特定的组件拥有的,并且继承自该状态的任何数据和UI只能影响在他们组件树之下的组件。
将组件树想象为一个属性的瀑布,每个组件的状态(state)就好比是在任意节点中加入的,同样是自上而下的额外水源。
为了表示所有的组件都是真正隔离的,我们可以创建一个App组件渲染三个<Clock>

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

在CodePen中尝试
每个Clock建立自己的定时器并独立更新。
在React应用中,组件是否有状态被认为是组件的实现细节,其可能会随着时间而改变。我们可以在有状态的组件中使用无状态的组件,反之亦然。