State和生命周期

想一下上一节中那个滴答计时的例子。
迄今为止,我们只学到一种更新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
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 (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

向类中添加本地状态

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

  1. render()方法中的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>
    );
  }
}
  1. 添加一个类的构造函数,初始化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. 将propdate<Clock />元素中移除:
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中被称作"挂载"。
同样的,我们也希望在生成Clock被移除的DOM时,清除掉这个计时器.这在React中被称为"取消挂载"。
我们可以在组件类里声明一些特殊的方法,在组件挂载和取消挂载的时候执行一些代码:

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有特殊的意义,除此之外,如果你需要存一些不用于显示的东西,可以自由地添加这些字段到类上面。
不再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上试一试

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

  1. 当<Clock />传给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向下传给它的子组件:

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

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

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

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

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

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容