状态(State) 和 生命周期

本页面介绍 React 组件中 状态(state) 和生命周期的概念。 你可以在这里找到 详细的组件API参考

考虑 前面章节 中的滴答时钟示例。 在 元素渲染 中,我们只学会了一种更新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);

在 CodePan 上尝试

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

在 CodePan 上尝试

然而,它没有满足一个关键的要求:Clock 设置定时器并每秒更新 UI ,事实上应该是 Clock 自身实现的一部分。

理想情况下,我们应该只引用一个 Clock , 然后让它自动计时并更新:

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

要实现这点,我们需要添加 stateClock 组件。

stateprops 类似,但是它是私有的,并且由组件本身完全控制。

我们 之前提到过, 用类定义的组件有一些额外的特性。 这个”类专有的特性”, 指的就是局部状态。

把函数式组件转化为类组件

你可以遵从以下5步, 把一个类似 Clock 这样的函数式组件转化为类组件:

  1. 创建一个继承自 React.Component 类的 ES6 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>
    );
  }
}

在 CodePan 上尝试

Clock 现在被定为类组件,而不是函数式组件。

每次更新发生时都会调用 render 方法,但只要我们将 <Clock /> 渲染到同一个 DOM 节点中,就只会使用 Clock 类的单例。 这让我们可以使用额外功能,例如本地状态(state) 和 生命周期钩子。

在类组件中添加本地状态(state)

我们现在通过以下3步, 把date从属性(props) 改为 状态(state):

  1. 替换 render() 方法中的 this.props.datethis.state.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')
);

在 CodePan 上尝试

接下来,我们将使 Clock 设置自己的计时器,并每秒更新一次。

在类中添加生命周期方法

在一个具有许多组件的应用程序中,在组件被销毁时释放所占用的资源是非常重要的。

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

这些方法称为 “生命周期钩子”。

componentDidMount() 钩子在组件输出被渲染到 DOM 之后运行。这是设置时钟的不错的位置:

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

注意我们把计时器ID直接存在 this 中。

this.props 由 React 本身设定, 而 this.state 具有特殊的含义,如果您需要存储不参与数据流的内容(如计时器 ID ),则可以自由向该类手动添加其他字段。

我们在 componentWillUnmount() 生命周期钩子中取消这个计时器:

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

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

它将使用 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')
);

在 CodePan 上尝试

现在这个时钟每秒都会走了。

我们来快速回顾一下该过程,以及调用方法的顺序:

  1. <Clock /> 被传入 ReactDOM.render() 时, React 会调用 Clock组件的构造函数。 因为 Clock 要显示的是当前时间,所以它将使用包含当前时间的对象来初始化 this.state 。我们稍后会更新此状态。

  2. 然后 React 调用了 Clock 组件的 render() 方法。 React 从该方法返回内容中得到要显示在屏幕上的内容。然后,React 然后更新 DOM 以匹配 Clock 的渲染输出。

  3. Clock 输出被插入到 DOM 中时,React 调用 componentDidMount() 生命周期钩子。在该方法中,Clock 组件请求浏览器设置一个定时器来一次调用 tick()

  4. 浏览器会每隔一秒调用一次 tick()方法。在该方法中, Clock 组件通过 setState() 方法并传递一个包含当前时间的对象来安排一个 UI 的更新。通过 setState(), React 得知了组件 state(状态)的变化, 随即再次调用 render() 方法,获取了当前应该显示的内容。 这次,render() 方法中的 this.state.date 的值已经发生了改变, 从而,其输出的内容也随之改变。React 于是据此对 DOM 进行更新。

  5. 如果通过其他操作将 Clock 组件从 DOM 中移除了, React 会调用 componentWillUnmount() 生命周期钩子, 所以计时器也会被停止。

正确地使用 State(状态)

关于 setState() 有三件事是你应该知道的。

不要直接修改 state(状态)

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

// 错误
this.state.comment = 'Hello';

setState() 代替:

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

唯一可以分配 this.state 的地方是构造函数。

state(状态) 更新可能是异步的

React 为了优化性能,有可能会将多个 setState() 调用合并为一次更新。

因为 this.propsthis.state 可能是异步更新的,你不能依赖他们的值计算下一个state(状态)。

例如, 以下代码可能导致 counter(计数器)更新失败:

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

要解决这个问题,应该使用第 2 种 setState() 的格式,它接收一个函数,而不是一个对象。该函数接收前一个状态值作为第 1 个参数, 并将更新后的值作为第 2 个参数:

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

我们在上面使用了一个箭头函数,但是也可以使用一个常规的函数:

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

state(状态)更新会被合并

当你调用 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(状态) 经常被称为 本地状态 或 封装状态的原因。 它不能被拥有并设置它的组件 以外的任何组件访问。

一个组件可以选择将 state(状态) 向下传递,作为其子组件的 props(属性):

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

同样适用于用户定义组件:

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

FormattedDate 组件通过 props(属性) 接收了 date 的值,但它仍然不能获知该值是来自于 Clock的 state(状态) ,还是 Clock 的 props(属性),或者是直接手动创建的:

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

在 CodePan 上尝试

这通常称为一个“从上到下”,或者“单向”的数据流。任何 state(状态) 始终由某个特定组件所有,并且从该 state(状态) 导出的任何数据 或 UI 只能影响树中 “下方” 的组件。

如果把组件树想像为 props(属性) 的瀑布,所有组件的 state(状态) 就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。

要证明所有组件都是完全独立的, 我们可以创建一个 App 组件,并在其中渲染 3 个 <Clocks>:

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

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

在 CodePan 上尝试

每个 Clock 都设置它自己的计时器并独立更新。

在 React 应用中,一个组件是否是有状态或者无状态的,被认为是组件的一个实现细节,随着时间推移可能发生改变。你可以在有状态的组件中使用无状态组件,反之亦然。