Fork me on GitHub

【译】快速起步-状态和生命周期

version: 15.4.2

状态和生命周期

到目前为止,我们仅学到了一种更新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);

CodePen地址

在本节中,我们将学习如何使得 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

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

想要实现这个目标,我们需要给 Clock 组件添加 state

stateprops 类似,但它是组件内部管理的的私有变量。

我们 之前提到,当使用 Class 定义组件时,可以使用一些附加的特性。本地状态就是这样的特性,仅在用 Class 定义组件时可用。

Function 转换为 Class

使用以下五步,可以将组件 Clock 从函数定义方式转换到类定义方式:

  1. 创建一个同名的继承自 React.ComponentES6 class

  2. 在类中添加空的 render()方法。

  3. 将函数体内的代码复制到 render() 方法中。

  4. render() 方法中使用 this.props 来替代 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 组件是用类定义的,而不是函数定义的。

这让我们可以使用一些附加特性,如本地状态和生命周期钩子。

添加本地状态到类

通过以下三个步骤,我们将会把 dateprops 移动到 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>
    );
  }
}
  1. 添加 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 到基类构造函数的:

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

类组件应始终传递 props 并调用基类构造函数。

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

CodePen地址

接下来,我们将使 Clock 启动自己的定时器,并每秒更新一次。

将生命周期方法添加到类

在包含多个组件的应用程序中,在组件释放时,销毁(清理)组件所占用的资源是非常重要的。

我们希望当 Clock 渲染到DOM时,第一时间 启动定时器。这个阶段在 React 中称之为 mounting

我们也希望当 Clock 产生的DOM被移除时 清除定时器 。这个阶段在 React 中称之为 unmounting

在组件的 mountingunmounting 阶段,我们可以定义特定的方法来运行我们的代码:

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

componentDidMount() 钩子在组件渲染到DOM之后运行,这是启动定时器的好地方:

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

注意我们是如何在 this 上保存定时器ID的

this.props 是由 React 自身管理的,this.state 也具有特定的意义。当您需要存储与渲染无关的内容时,也可以手动的在类中添加其他字段。

render() 无关的内容,没必要存储在 state 中。

我们将会在 componentWillUnmount 生命周期钩子中清除定时器:

componentWillUnmount() {
    clearInterval(this.timerID);
  }

最后,我们将实现每秒运行的 tick() 方法。

它将使用 this.setState() 来实现组件本地状态的更新:

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地址

现在 Clock 会每秒跳动。

我们来简单回顾下发生了什么以及调用方法的顺序:

  1. <Clock /> 传递给 ReactDOM.render() 时,React 调用了 Clock 组件的构造函数。由于 Clock 需要显示当前时间,我们通过一个包含当前时间的对象来初始化 this.state。稍后,我们将更新这个 state

  2. 接下来,React 调用 Clock 组件的 render() 方法。这决定了 React 将会在屏幕上显示什么。接着,React 将组件的输出更新到DOM上。

  3. Clock 组件被添加到DOM之后,React会调用 componentDidMount() 钩子函数。 在它的内部, Clock 组件启动浏览器定时器,并每秒执行一次 tick()

  4. 每一秒,浏览器都会调用 tick() 方法。在它的内部,Clock 组件将包含当前时间的对象传递给 this.setState(),并以此来调度UI变更。当调用 setState() 方法,React 知道了 state 变化,然后就调用 render() 来确定要在屏幕上渲染的内容。这个时候, render() 中的 this.state.date 将和之前不同,会产生包含当前时间的新的输出。于是,React则对应的更新DOM。

  5. Clock 组件从DOM移除时,React 将调用 componentWillUnmount() 来停止定时器。

正确的使用 state

关于 setState(),您需要知道以下三点:

不要直接修改 state

如下代码将不会重新渲染组件:

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

请使用 setState() 替代:

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

唯一可以给 this.state 赋值的地方是构造函数。

状态更新可能是异步的

为了提高性能,React可能会在单个更新中批量处理多个 setState()

由于 this.propsthis.state 可能是异步更新的,您不应该依靠它们的值来计算下一个状态。

以下代码可能不会正确更新计数器:

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

要解决这个问题,可以 setState() 的第二种形式,接受一个函数。这个函数的第一个参数是上一个状态,第二个参数是当前属性:

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

以上代码我们使用了 arrow function ,但常规函数也是可以正常使用的:

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

状态是合并更新

当你调用 setState() 时,React 会将您传递的对象合并到当前状态。

您的状态可能包含几个独立变量,如下:

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

数据流向

父组件和子组件都不必要知道某个组件是有状态的还是无状态的,并且它们不应该关心它是否被定义函数或类。

这也是为什么 state 通常被称为本地状态或封装状态。因为除了组件本身之外,其他组件都是不能直接访问组件状态的。

父组件可以选择将状态通过子组件的属性进行传递:

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

This also works for user-defined components:

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

FormattedDate 组件将通过它自身的 props 接收到 date,不需要知道 date 是来自哪里(Clock 组件的状态或属性,乃至手动输入):

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

CodePen地址

这通常被称作 从上到下单向 数据流。任何 state 始终由某个特定组件拥有,并且从该 state 导出的任何数据或UI都只会影响子集。

如果把组件树想象为瀑布,每个组件的状态都是一个额外的水源,它可以从任一点汇入,但都向下流动。

为了表明组件都是相互隔离的,我们可以创建一个包含三个 Clock 组件的 App 组件:

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

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

CodePen地址

每个 React 有自己的定时器,并独立更新(并不会相互影响)。

React 应用中,组件有无状态被认为是可变的,它是组件的具体实现细节。你可以在有状态组件中使用无状态组件,反之亦然。

posted @ 2017-04-14 17:54  幻天芒  阅读(435)  评论(0编辑  收藏  举报