建议配合官网文档 State and Lifecycle 进行阅读。

State and Lifecycle

这一节将会介绍state, lifecycle的概念,你可以在detailed component API reference找到更详细的讲解。

在之前的章节中,我们有一个有一个可以显示时间的元素,当时我们的更新方式为重新创建一个新元素,并调用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);

在这一节,我们将学习如何时将这个时间元素变得可重用,并且封装它。我们的封装如下所示:

// 时钟函数组件
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);

这种封装方式有一个至关重要的问题,即时钟的更新时间setInterval()应该属于Clock组件的内部细节,但上述代码并没有将其放在Clock内部,为了实现该功能,我们向Clock中引入state
state 和 props 十分类似,但 state 是私有的,并且完全由组件控制,即可以类可以修改自己的 state.

Converting a Function to a Class

在需要使用 state 时,一般的做法是使用类组件,因此我们先将函数组件改写为类组件:

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
      );
  }
}

我们知道,无论是类组件还是上面封装的函数组件,对于 React 来说都是等价的,在这种情况下,每次更新时 React 都会自动的调用render()函数,但只会渲染在同一个 DOM 上,并不会像之前的章节那样每次都创建一个新实例。

下面,我们使用类组件中的 state 进行更好的封装。

Adding Local State to a Class

对于一个可复用的时钟来说,刷新的时间间隔最好自己的属性,而不是参数,因此我们通过如下方式将 props 变成 state:

  1. 在类中添加构造函数,初始化其state:
    class Clock extends React.Component {
    

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

/* render函数 */
}:hexoPostRenderEscape–>
> 构造函数constructor()super(props)不能省略,该语句表示初始化父类。
2. 将this.props.date变成this.state.date

class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

3. 将传递给Clock的 props 删除,我们可以得到如下代码:
class Clock extends React.Component {
  /* 构造函数,用来初始化 satate */
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
  /* render函数,用来返回希望显示的 UI 描述 */
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      </div>
    );
  }
}
/* 将组件渲染到根节点上 */
ReactDOM.render(
  /* 这里的 Clock 不再接收 date="new Date()" */
  <Clock />,  document.getElementById('root')
);

最后,我们将计时器添加到组件本身。

Adding Lifecycle Methods to a Class

对于组件来说,当被销毁时,释放资源是十分重要的。
我们希望当Clock被创建时,开启一个计时器,这在 React 中被称为挂载 mounting
我们也希望当Clock被销毁时,能结束这个定时器,这咋 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(), componentWillUnmount()就被称作生命周期方法lifecycle methods.
componentDidMount()会在组件完成 DOM 渲染后执行,因此在这里开启定时器时合理的:

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

这里我们将定时器变量保存在this.timerID中,相较与 React 内置的 this.props, this.state,只要是不参加数据流动的变量,我们都可以自由的添加在类组件中,就如this.timerID一样。

我们将会在componentWillUnmount()中销毁启动的定时器:

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

最后,我们会实现在定时器中回调的tick()的函数:

tick() {
  this.setState({
    date: new Date()
  });
}

最终,Clock被封装成:

// Clock 类组件
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);
  }

  // 定时器中的回调函数,用来更新 state 中的 date
  tick() {
    this.setState({
      date: new Date()
    });
  }

  // 渲染函数,用来输出希望在 UI 中呈现的内容
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

// 将 Clock 渲染到根节点上
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

现在,让我们从头梳理一下函数的调用顺序:

  1. <Clock />被传递到ReactDOM.render()中, React 调用 Clock的构造函数新建一个Clock实例,在构造函数constructor()中初始化Clock中的state
  2. React 调用Clock中的render()函数,通过其输出(返回的元素),React 更新 DOM 以匹配我们预期的结果。
  3. Clock确实插入到 DOM 中,React 调用componentDidMount()生命周期函数,在该函数里,我们开启了定时器,并将其命名为timerID绑定在实例中,该定时器会以1000毫秒为间隔重复调用tick()函数。
  4. 每次tick()函数被调用时,Clock会根据setState()函数对state的更新,更新 UI,由于 setState()函数的调用,React 可以知道何时state被修改了,因此 React 在state被修改时会自动重新调用render()函数。
  5. 如果Clock组件从 DOM 中移除,则在移除前会调用componentWillUnmount()生命周期函数,在该函数里,定时器将被清除。

Using State Correctly

在使用state时,需要注意以下三点:

Do Not Modify State Directly

由于React 是通过setState()来感知何时state被修改了,因此直接修改state可能不会使得组件被重新渲染:

// Wrong, 没有通过 setState() 修改state,组件将不会重新渲染
this.state.comment = 'Hello';

因此正确的做法是使用setState()进行 state的修改:

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

this.state =的语法只能用在构造函数constructor()中以初始化state.

State Updates May Be Asynchronous

React 可能将多个setState函数批量调用以提升性能,因此setState()的更新可能是异步的,因此通过当前的state来计算下一state是不可靠的:

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

为了避免这个问题,当新的state需要依赖当前state时,不使用对象而采用函数的方式可以避免这个问题:

this.setState(
  // 这是仅存在返回语句的箭头函数的简写
  // 等价于 function(state, props) { return {counter: state.counter + props.increment}}
  // 实际上 setState 接收到的还是一个对象
  (state, props)=> ({
    counter: state.counter + props.increment
  })
)

State Updates are Merged

setState()被调用时,React 会将你提供的对象与当前的state对象进行合并,也就是说,只有你提供的k-v会被修改,未提供的不会发生改变:

The Data Flows Down

对于任意一个组件,其父组件和子组件以及其余任何组件都无法获取它的state,因此state是局部的、封装的。

但是一个组件可以选择向其子组件(向下)传递它的state,即作为子组件的props

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

FormattedDate组件接收date作为它的props,它并不关心props中的数据是否来自父组件的state,它只需要通过props使用即可:

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

这种数据流通常被称为单向数据流或数据向下流动。

每个组件都是独立的,如果我们创建三个Clock组件,它们对拥有各自的计时器,并独立的进行更新,互不影响。

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

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