State和生命周期

想一下上一节中那个滴答计时的例子。
迄今为止,我们只学到一种更新UI的方法。
我们通过调用ReactDOM.render()来改变已经渲染的输出:

function tick() {
  const element = (
    

Hello, world!

It is {new Date().toLocaleTimeString()}.

); ReactDOM.render( element, document.getElementById('root') ); } setInterval(tick, 1000);

在CodePen上试一试
在本节里,我们将学会做一个真正可重用且有封装良好的Clock组件。
我们可以从封装时钟的外观开始:

function Clock(props) {
  return (
    

Hello, world!

It is {props.date.toLocaleTimeString()}.

); } function tick() { ReactDOM.render( , document.getElementById('root') ); } setInterval(tick, 1000);

在CodePen上试一试
不过,上面的代码漏了一个关键的需求:Clock应该自己去设置计时器,并且每秒更新UI。
理想情况下,我们想要只写一次,并且让Clock去更新自己:

ReactDOM.render(
  ,
  document.getElementById('root')
);

为了实现这个功能,我们需要为Clock组件添加state
State和props类似,但他是私有的,并完全由组件控制。
正如我们之前提到的,使用类定义的组件有一些额外的特性。本地的state就是这样一个特性:只能通过类来开启。

将函数转换成类

你可以在五个步骤内将一个像Clock一样的功能化组件转为一个类:

  1. 创建一个扩展自React.Component的同名ES6类。
  2. 添加一个名为render()空的方法。
  3. 将函数的内容移到render()方法中。
  4. render()中的props替换成this.props
  5. 删除剩下的空函数声明。
class Clock extends React.Component {
  render() {
    return (
      

Hello, world!

It is {this.props.date.toLocaleTimeString()}.

); } }

在CodePen上试一试
Clock现在就是通过类来定义的,而非函数喽。
现在我们就可以添加诸如本地状态和生命周期钩子等额外的特性了。

向类中添加本地状态

我们将date从props中移动到状态中:

  1. render()方法中的this.props.date替换为this.state.date
class Clock extends React.Component {
  render() {
    return (
      

Hello, world!

It is {this.state.date.toLocaleTimeString()}.

); } }
  1. 添加一个类的构造函数,初始化this.state
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      

Hello, world!

It is {this.state.date.toLocaleTimeString()}.

); } }

注意我们是如何将props传给父类构造函数的:

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

组件类应该始终调用父类的构造函数,并传入props

  1. 将propdate元素中移除:
ReactDOM.render(
  ,
  document.getElementById('root')
);

稍后我们将计时器代码加回组件内。
现在结果看起来:

class Clock extends React.Component {
    constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      

Hello, world!

It is {this.state.date.toLocaleTimeString()}.

); } } ReactDOM.render( , document.getElementById('root') );

在CodePen上试一试
下面我们会让Clock设置它自己的计时器,每秒自己去进行更新。

在类中添加生命周期方法

在有很多组件的应用中,当组件被销毁时,组件可以释放自己管理的资源是非常的重要。
我们希望Clock在第一次被渲染到DOM时设置一个计时器。这在React中被称作"挂载"。
同样的,我们也希望在生成Clock被移除的DOM时,清除掉这个计时器.这在React中被称为"取消挂载"。
我们可以在组件类里声明一些特殊的方法,在组件挂载和取消挂载的时候执行一些代码:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {

  }

  componentWillUnmount() {

  }

  render() {
    return (
      

Hello, world!

It is {this.state.date.toLocaleTimeString()}.

); } }

这些方法称为"生命周期钩子"。
钩子componentDidMount()在组件被渲染到DOM后被执行。在这里设置计时器非常合适:

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

注意我们如何将计时器的ID保存到this上的。
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 (
      

Hello, world!

It is {this.state.date.toLocaleTimeString()}.

); } } ReactDOM.render( , document.getElementById('root') );

在CodePen上试一试

现在时钟每秒滴答一次。
让我们快速的回顾下发生了什么,还有这些方法调用的顺序:

  1. 传给ReactDOM.render()时,React调用Clock组件的构造函数。因为Clock需要显示当前的时间,他使用一个包含当前时间的对象来初始化this.state。我们稍后更新这个state。
  2. 接着,React调用Clock组件的render()方法。React由此得知那些东西应该显示在屏幕上。然后React更新DOM来使之匹配Clock的渲染输出。
  3. Clock的输出被插入DOM中,React调用componentDidMount()生命周期钩子。在这个钩子中,Clock组件将请求浏览器设置一个计时器每秒调用tick()
  4. 浏览器每秒调用一次tick()方法。在这个方法中,Clock组件通过调用setState()(传入一个包含当前时间的对象),来调度UI的更新。就是因为调用了setState(),React才得知状态发生了变化,然后去再次调用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()的第二种形式(参数是一个函数,而不是一个对象)可以修复这种情况。这个函数将上个状态作为第一个参数,这次更新时的props作为第二个参数:

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

上面我们用到了箭头函数,但用普通的函数也可以。

// 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

向下的数据流

一个组件的父子都不会知道该组件是有状态的还是无状态的,而且他们不会去关心它是以函数还是类的形式定义的。
这就是为什么状态被称作本地或封装的。只有拥有、设置它的组件才可以访问它。
一个组件可以选择将它的状态作为props向下传给它的子组件:

It is {this.state.date.toLocaleTimeString()}.

这对自定义组件同样有效:


FormattedDate组件从它的props中接收date,而他并不知道这个数据来源到底是Clock的状态,还是Clock的props,亦或手动输入的:

function FormattedDate(props) {
  return 

It is {props.date.toLocaleTimeString()}.

; }

在CodePen上试一试
这通常被称为“自上而下”或“单向”数据流。任何状态始终由某个特定组件拥有,从该状态导出的数据或UI只能影响树状结构中他下面的组件。
如果你将一个组件树想象成一个props的瀑布,那么每个组件的状态就像一个额外的水源,它可以在任意一点加入,但也是向下流动。
为了表明组件间真的都是独立的,我们可以创建一个渲染三个App组件:

function App() {
  return (
    
); } ReactDOM.render( , document.getElementById('root') );

在CodePen上试一试
每个Clock设置自己的计时器,并且独立更新。
在React应用中,组件是否有状态,是作为组件的实现细节来考虑的,可能随着时间会发生变化。你可以在有状态的组件中使用无状态的组件,反之亦然。

你可能感兴趣的:(State和生命周期)