玩转 React(五)- 组件的内部状态和生命周期

文章标题总算是能够正常一点了……react

经过以前的文章咱们已经知道:在 React 体系中所谓的 "在 JavaScript 中编写 HTML 代码" 指的是 React 扩展了 JavaScript 的语法,也就是 JSX。JSX 语法中能够以相似 HTML 语法的方式使用 React 组件,从而编写 React 组件就有一种创造一个新的 HTML 标签的体验。web

上一篇文章《玩转 React(四)- 创造一个新的 HTML 标签》介绍了如何来建立一个 React 组件,以及组件的属性。了解到组件的视图是属性的映射,经过改变组件属性能够触发组件从新渲染,从而改变组件的视图。其实组件的视图并不只仅是由属性映射来的,本篇将介绍另外一种能够触发组件从新渲染的方式,即组件的内部状态(state),严格来讲组件的视图是由属性和内部状态映射而来的,即:view = f(props, state),跟属性相似,状态的改变也会触发组件从新渲染,只不过状态是组件内部基于自身逻辑或者用户事件本身维护的,而不是由外部输入的。ajax

另外本文中会介绍一个经过类继承方式定义的组件的生命周期,以及在各个生命周期函数中能作什么,不能或尽可能不要作什么。segmentfault

内容摘要

  • ReactDOM.render 在一个单页面 web 应用中一般只调用一次。
  • 组件能够经过 setState 改变内部状态 state 来更新视图。
  • setState 多数状况下是异步的。
  • 不要直接使用当前 state 的值生成下一个 state
  • 不要直接经过 this.state 修改 state
  • 组件生命周期流程图。
  • 各个生命周期函数介绍及使用经验。

以上是本文的内容摘要,若是你已经知道我要说的是什么,那么就没有必要继续看下去了,节约时间。bash

组件的内部状态

此前,咱们已经了解到能够经过 ReactDOM.render(<HelloMessage name="Lucy" />, container) 的方式,将带有特定属性的组件渲染到页面的某个 DOM 节点中(container),这样页面上会展现出 “Hello Lucy”,当咱们但愿页面上展现 “Hello Tom” 的时候,咱们能够将组件的 name 属性改成 Tom 后再次调用 ReactDOM.render 方法,这样组件就会以新的属性从新渲染,从而更新组件的视图。微信

下面是官方文档中一个展现时钟的例子,我简单改造了下:网络

codepen.io/Sarike/pen/…less

例子中定义了一个 Clock 组件,组件接收一个 time 属性,在组件外部经过 setInterval 周期性地调用 ReactDOM.render 不断更新 Clock 的属性并从新渲染。异步

然而在不少实际场景中,对于一个时钟组件,咱们但愿它有更好的封装性和复用性,也就是说咱们但愿只调用一次 ReactDOM.render(<Clock />, container) 而后它能够本身更新本身的视图,这样咱们就更容易在页面上放置多个时钟了,即复用性更好了。函数

要达到这个目的,就须要组件的内部状态来支持。组件有一个特殊的属性 state 用来保存组件的内部状态。用户能够经过 this.setState(statePatch) 来更新组件的状态,组件的状态更新后会从新执行 render 方法来更新视图,上面的例子使用内部状态改造后:

codepen.io/Sarike/pen/…

这样 Clock 做为一个完整的时钟组件就能够本身来更新本身了,上篇文中也有提到过,若是想要使用组件的内部状态,那组件必须以类继承的方式来定义,而不能使用函数式组件。因此说,函数式组件常常也被称做是无状态组件(stateless)。

上面例子中有用到 componentDidMountcomponentWillUnmount 两个函数,它们是组件的生命周期函数,本文的后半部分将会介绍,这俩函数分别在组件挂载到页面上和组件将要从页面上移除时调用。

改造后的例子,咱们只须要调用一次 ReactDOM.render 便可,在实际的项目中,一个完整的单页面 web 应用,也只须要调用一次 ReactDOM.render 方法把根组件挂载到页面中便可,剩下的工做就都放心地交给 React 就好了。

初始化组件内部状态

在建立一个拥有内部状态的组件时,咱们须要对内部状态进行初始化,即设置组件最初的状态是什么。作法很简单,就是在构造函数 constructor 中设置 state 属性就能够了。以下所示:

class MyComponent extends React.Component {
    constructor(props) {
        super(props); // 这行代码不能少哦
        this.state = {
            name: "Lucy"
        }
    }
}复制代码

setState 大多数状况下是异步的

setState 多数状况下是异步的,异步意味着经过 setState 更新组件状态后,不能马上经过 this.state 来获取到更新以后的值,另外当连续屡次调用 setState 来更新同一个字段时,只有最后一次更新才会生效。以下示例:

codepen.io/Sarike/pen/…

若是但愿上面示例代码正常工做,你须要经过回调函数的方式来生成下一个 state,以下所示:

this.setState(preState => ({value: preState.value + 1}));
this.setState(preState => ({value: preState.value + 2}));
this.setState(preState => ({value: preState.value + 3}));复制代码

因此,直接基于当前 state 的值,生成一下个 state 是不靠谱的,可是不少不清楚这一点的同窗基本上都是这么作的,由于写起来简单嘛,并且貌似也没有什么问题。这是由于不少状况下,业务逻辑没有那么复杂,基本不会频繁调用 setState 。可是这确实是一个隐患,若是在项目初期不注意规避,等项目复杂到必定程度之后,可能会出现难以排查的BUG。

那为何说多数状况下是异步的呢?难道有些状况下不是异步的吗?是的,实际上只有在 React 能控制的事件处理过程当中调用的 setState 才是异步的,如:生命周期函数,React 内置的如 button,input 等组件的事件处理函数。在多数的状况下咱们只须要在这些地方控制咱们的组件就够了,因此说大多数状况下 setState 是异步的。

在某些特殊的组件中,可能须要经过 addEventListener 来设置某些 DOM 的事件处理函数,在这种经过原生的 JS API 来设置的事件处理过程调用 setState 就是同步的,会当即更新 this.state。另外还有 setIntervalsetTimeout 等原生 API 的回调函数也是如此。

参考:www.zhihu.com/question/66…

不要直接经过 this.state 来更新组件状态

这一点跟属性相似,直接经过 this.state 修改组件状态,组件状态被修改了,但并不会触发组件的从新渲染。这样就会致使组件视图与状态不一致。

生命周期函数

一个组件被咱们创造到这个世界上以后,在使用它时,它的每一个实例都是有必定生命周期的,下面这张图说明了一个组件实例的生命周期:

生命周期
生命周期

图片来源:tylermcginnis.com/an-introduc…

这张图略微有点老,不过结合下文来看也没什么问题,下面咱们来解释一下上面这张图。

组件初始化:constructor

咱们定义的每个组件,都是一个类(class),这些类被实例化后才能做为 React DOM 中的一个节点渲染到页面上。因此,当咱们经过 ReactDOM.render 或者在某个组件中经过 JSX 表达式将一个组件第一次渲染到页面上时,组件首先要作的就是对组件进行实例化。

实例化主要作的事情:

  • 建立一个组件的实例对象(也就是 Element,一般对应一个JSX表达式,如:<MyComponent />)。
  • 获取组件的默认属性。
  • 获取组件的初始内部状态(在 constructorthis.state = xxxx;)。

componentWillMount

在组件被渲染到页面上以前执行,在组件的整个生命周期内只执行一次。在这里能够调用 setState 更新内部状态,可是更推荐将这里的状态更新操做放到 constructor 中。

该函数执行完后会立马执行 render 方法并将组件渲染到页面上。因此,在这里执行 setState 不会触发额外的渲染过程,由于这是没有必要的。

componentDidMount

组件被渲染到页面上后立马执行,在组件的整个生命周期内只执行一次。这个时候是作以下操做的好时机:

  • 某些依赖组件 DOM 节点的操做。
  • 发起网络请求。
  • 设置 setIntervalsetTimeout 等计时器操做。

在这里能够调用 setState 更新组件内部状态,且会触发一个从新渲染的过程,即会从新执行 render 方法并更新视图。

componentWillReceiveProps

componentWillReceiveProps(nextProps)复制代码

该声明周期函数可能在两种状况下被调用:

  1. 组件接收到了新的属性。新的属性会经过 nextProps 获取到。
  2. 组件没有收到新的属性,可是因为父组件从新渲染致使当前组件也被从新渲染。

你只要知道,当该函数被调用时,并不必定是由于属性发生了变化

在这里也能够调用 setState 更新组件的内部状态,一样也不会触发额外的从新渲染操做,React 会聪明地用更新后的属性和内部状态进行一次从新渲染。

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState)复制代码

这是一个询问式的生命周期函数,因此该函数须要一个返回值 true/false,若是为 true,组件将触发从新渲染过程,若是为 false 组件将不会触发从新渲染。所以,合理地利用该函数能够必定程度节省开销,提升系统的性能。

此处不能调用 setState 更新组件的状态。

因为组件属性或者内部状态被改变时都触发组件从新渲染,因此该函数接受两个参数:新的属性(nextProps)、新的状态(nextState)。

在处理该声明周期函数时,切记要兼顾属性和状态,不能只顾其一,否则很容易踩坑。例如:某位同窗只依据属性来判断是否触发从新渲染,而忽略了内部状态,这样就致使你不管如何 setState,组件视图都不能正常更新。

在上篇文章中咱们提到类继承方式定义组件时说到,React 提供了两个基类,一个是 Component,另外一个是 PureComponent,二者的差异就在于后者已经帮咱们简单实现了一下 shouldComponentUpdate 函数,当属性和状态都没有发生变化时返回 false 以免额外的开销。

可是比对过程出于性能考虑,只是进行浅比对,也就是只比对对象的第一级字段,并且是否发生变化是经过 Object.is 方法类判断的。因此会致使有时候发生变化了组件没有更新,没有变化却触发了从新渲染过程。这个在这里再也不赘述,想深刻探讨能够扫描问候的二维码加我微信好友(个人微信:leobaba88)。

componentWillUpdate

当组件 shouldComponentUpdate 返回 true 或者调用 forceUpdate 时将触发此函数。

该函数中不能调用 setState 更新组件状态,当你想这么作的时候,你能够考虑将它移到 componentWillReceiveProps 函数里。

该函数在函数第一次渲染的时候不会执行。

componentDidUpdate

componentDidUpdate(prevProps, prevState)复制代码

在组件从新渲染过程当中,从新执行 render 方法并更新组件视图后当即执行该函数。相似组件第一次渲染过程当中的 componentDidMount,该函数在第一次渲染时不会执行。

在此处是作这些事情的好时机:

  • 执行依赖新 DOM 节点的操做。
  • 依据新的属性发起新的网络请求。(可是此处必定要格外谨慎,必定要在确认属性变化后再发起网络请求,否则极有可能进入死循环:didUpdate -> ajax -> changeProps -> didUpdate -> ...)。

componentWillUnmount

当组件被从页面中移除以前调用,此时是清理战场的好时机,如清理定时器、中指网络请求等。

componentDidCatch

componentDidCatch(error, info)复制代码

这是 React 16 新加入的一个生命周期函数。定义该生命周期函数的组件将会成为一个错误边界,错误边界这个词很是形象,它能够有效地将错误限制在一个有限的范围内,而不会致使整个应用崩溃,防止一颗耗子屎坏了一锅汤。

错误边界组件,能够捕获其整个子组件树内发生的任何异常,可是却不能捕获自身的异常。

下面是官方的一个示例,你们感觉下:

codepen.io/gaearon/pen…

最后(微信群)

这篇文章来的有点慢,很是抱歉。

另外为了方便你们阅读,我将全部文章的连接更新到第一篇文章 《玩转React(一)- 前言》 中。

文字的表现范围毕竟有限,为了方便你们交流,我建了一个微信群,对 React 感兴趣的同窗能够进群一块儿交流、学习,因为微信群邀请的时间限制,你们能够先扫描下面二维码,加我好友,我拉你们进群:

clipboard.png
clipboard.png

个人微信:leobaba88

相关文章
相关标签/搜索