考虑下 之前章节学习的时钟的例子。在本章,我们将要学习怎样真正复用和封装这个‘时钟’,它会拥有自己的计时器并没秒自己更新自己。

到目前为止,我们只有一种途径更新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);

在本章,我们将要学习怎样真正复用和封装这个‘时钟’,它会拥有自己的计时器并没秒自己更新自己。

让我们开始于封装它的外观。

  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);

不过这样少了重要的一点:实际上,钟表应该有这么个细节实现,那就是钟表自身维护一个计时器然后每秒更新自己的UI。

理想情况是,我们想一次性写成然后让它自己更新:

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

为了实现这样,我们需要为组件引入State(状态)。

State(状态)有点像props(属性),但是state状态是私有的,而且完全由组件自身管理。

我们之前谈到过,说组件如果定义成了class形式(不是函数式)将会有很多额外的特性。那么State就是这样:一种只有class形式定义组件才可用的特性。

把函数式转换成class形式

把一个类似钟表的组件,从函数式转换一个class形式,需要5个步骤:

1.创建一个es6新特性的class,和函数同名且继承自React.Component;

2.给它添加一个叫render()的新方法;

3.把函数体放到render()方法里;

4.把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. }

‘时钟’组件现在是CLass的而不是函数式了。

那就让我们使用额外诸如local State(本地状态)和lifecycle hook(生命周期回调钩子)的新特性吧。

在class的基础上增加State状态

我们把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构造器并初始化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. }

class组件总是带着props去调用基础构造器

3.从元素中移除date属性:

  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. );

下面,我们将把定时器的代码再添加回组件自身并每秒更新它。

添加Class的生命周期方法

应用Application是由很多组件组成,那么在当一个组件销毁时,把它所持有的资源释放就显得尤为重要。

我们写这样一个计时器:在最开始的时候Clock被渲染到DOM上的时候’安装’计时器,这在React里面叫mounting(挂载)。

同样我们在Clock移除的时候清除这个计时器,这在React里面叫unmounting(卸载)。

我们在组件类的一些特殊的方法中写一些代码,那么当组件卸载或者挂载的时候代码就会执行:

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

这些方法叫声明周期方法(钩子函数)。

方法:componentDidMount()在组件被渲染到DOM之后调用,这是个安装定时器的好时机:

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

注意我们怎样在this的右边保存定时器的ID(定时器的ID是定时器的标志)。

好吧,既然this.props是被React自己调用的,this.state有特殊的含义,如果你还需要存储一些其他不需要在显示层输出的信息,那么你也可以自主地随便加些字段属性。

当componentWillUnmount()调用的时候我们销毁定时器:

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

最后,我们实现一个tick()方法,让组件每秒都运行这个方法.

它将使用this.setState()来调度更新组件的当前状态。

  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. );

现在,它每秒都在运行。

让我们快速回顾发生了什并整理方法调用的次序:

1.当被传递到ReactDOM.render(),React调用了组件Clock的构造方法.组件需要呈现当前时间,它初始化了this.state状态,初始化的值是一个包含了当前时间的对象。我们稍后会更新这个状态。

2.React然后调用了Clock组件的render方法。这就是为啥React知道往页面上呈现什么的原因。React而后会根据的输出更新DOM。

3.当Clock的输出插入到DOM的时候,React调用了生命周期方法:componentDidMount(),在方法里面,Clock组件去委托浏览器每一秒都调用组件的方法tick()一次。

4.每当浏览器调用了一次tick()方法。Clock组件通过setState()设置一个包含当前时间的对象来调度UI更新。感谢这个setState()调用,React得以知晓state状态发生了变化,然后就再次调用render()方法更新其所呈现。这时,this.state.date在render()方法里面已经发生了变化,所以渲染输出就是当前的时间,React于是相应的更新DOM。

5.如若Clock最终从DOM中被移除,React就调用componentWillUnmount()保证定时器被停止。

正确使用状态

关于setState(),你得了解3点:

不要直接更新状态

举例,这样不会重新渲染一个组件:

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

取而代之,用setState():

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

只有一个地方可以允许你直接设置属性,那就是构造器里。

状态的更新可能不同步(异步)

React有可能会收集一批setState()调用以后统一更新。

那是因为this.props和this.state也可能不同步,你不能依靠这些值去推算下一个状态。

举例来说,用下面的代码来更新计数器counter可能要失败:

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

修复它,需要用setState()的另一种形式:setState()接受一个函数作为参数而不是对象。那个作为参数的函数的第一个参数是先前状态(previous state),第二个参数是当前属性:

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

上面我们用到了箭头函数,不过用常规的函数它也能正常工作:

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

状态的更新是合入式的

当你调用setState(),React将你提供的对象合入当前的状态。

举例说,你的状态可能包含了一些彼此独立的变量:

  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.posts的完整,但却完全代替了过去的this.state.comments。

数据向下走

不管是子组件还是父组件,都不知道一个确定的组件彼是有状态还是没有状态,并且他们也不关心定义方式是函数式还是类形式。

这就是为什么我们把state状态叫做本地的或者封装的。因为对于别的组件来说它们是不可访问的。

一个组件可以选择把自己的state状态以props属性的方式下发给子组件:

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

这同样适用于用户自定义组件中:

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

组件FormattedDate收到data在它的props中,而且不知道是否data来自于Clock的state或者props或者其他:

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

这就是通常所说的”自顶向下”或者说”单向性”(unidirectional)数据流。任何状态都是属于一些特殊的组件,任何数据或由状态衍生的UI只能影响那些下层的组件。

如果你将组件树看作是props瀑布,那么每一个组件的state就像是那额外的水源,这些水源在任意点加入瀑布并只向下流动。

为了展示所有的组件都是完全孤立的,我们创建了一个APP组件,渲染了三个

  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. );

在React APP中,一个组件有很多状态或者一个状态也没有是由组件的实现细节决定,而这些细节也是可能变化的。你可以把丰富状态的组件放到鲜有状态的组件之中,反之亦然。

官网文章 Quick Start :State and LifeCycle