深刻React的生命周期(上):出生阶段(Mount)

前言

本文是对开源图书React In-depth: An exploration of UI development的概括和加强。同时也融入了本身在开发中的一些心得。javascript

你或许会问,阅读完这篇文章以后,对工做中开发React相关的项目有帮助吗?实话实说帮助不会太大。这篇文章不会教你使用一项新技术,不会帮助你提升编程技巧,而是完善你的React知识体系,例如区分某些概念,明白一些最佳实践是怎么来的等等。若是硬是要从功利的角度来考虑这些知识带来的价值,那么会是对你的面试很是有帮助,这篇文章里知识点在面试时经常会被问到,为何我知道,由于我吃过它们的亏。html

React组件的生命周期划分为出生(mount),更新(update)和死亡(unmount),然而咱们怎么知道组件进入到了哪一个阶段?只能经过React组件暴露给咱们的钩子(hook)函数来知晓。什么是钩子函数,就是在特定阶段执行的函数,好比constructor只会在组件出生阶段被调用一次,这就算是一个“钩子”。反过来讲,当某个钩子函数被调用时,也就意味着它进入了某个生命阶段,因此你能够在钩子函数里添加一些代码逻辑在用于在特定的阶段执行。固然这不是绝对的,好比render函数既会在出生阶段执行,也会在更新阶段执行。顺便多说一句,“钩子”在编程中也算是一类设计模式,好比github的Webhooks。顾名思义它也是钩子,你可以经过Webhook订阅github上的事件,当事件发生时,github就会像你的服务发送POST请求。利用这个特性,你能够监听master分支有没有新的合并事件发生,若是你的服务收到了该事件的消息,那么你就能够例子执行部署工做。java

咱们按照阶段的时间顺序对每个钩子函数进行讲解。react

出生

  • constructor
  • getDefaultProps() (React.createClass) orMyComponent.defaultProps (ES6 class)
  • getInitialState() (React.createClass) or this.state = ... (ES6 constructor)
  • componentWillMount()
  • render()
  • componentDidMount()

首先咱们要引入一个概念:组件(Component)。组件很是好理解,就是能够复用的模板。例如经过按钮组件(模板)咱们能够实例化出多个类似的按钮出来。这和代码中类(Class)的概念是相同的。而且在ES6代码中定义组件时也是经过类来实现的:git

import React from 'react';

class MyButton extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <button>My Button</button>
    )
  }
}复制代码

也能够经过ES2015的语法接口React.createClass来定义组件:github

const MyButton = React.createClass({
  render: function() {
    return (
      <button>My Button</button>      
    );
  }
});复制代码

若是你的babel配置文件.babelrcpresets指定了es2015,那么在编译以后的文件中,你会发现class MyButton extends React.Component语句编译以后的结果就是React.createClassweb

注意到当咱们在使用class定义组件时,继承(extends)了React.Component类。但实际上这并非必须的。好比你彻底能够写成纯函数的形式:面试

const MyButton = () => {
  return <h1>My Button</h1>
}复制代码

这就是无状态(stateless)组件,顾名思义它是没有本身独立状态的,这个概念被用于React的设计模式:High Order Component和Container Component中。具体能够参考个人另外一篇文章面试系列之三:你真的了解React吗(中)组件间的通讯以及React优化编程

它的局限也很明显,由于没有继承React.Component的缘故,你没法得到各类生命周期函数,也没法访问状态(state),可是仍然可以访问传入的属性(props),它们是做为函数的参数传入的。设计模式

定义组件时并不会触发任何的生命周期函数,组件本身也并不会存在生命周期这一说,真正的生命周期开始于组件被渲染至页面中。

让咱们看一段最简单的代码:

import React from 'react';
import ReactDOM from 'react-dom';

class MyComponent extends React.Component {
  render() {
    return <div>Hello World!</div>;
  }
};

ReactDOM.render(<MyComponent />, document.getElementById('mount-point'));复制代码

在这段代码中,MyComponnet组件经过ReactDOM.render函数被渲染至页面中。若是你在MyComponent组件的各个生命周期函数中添加日志的话,会看到日志依次在控制台输出。

为了说明一些问题,咱们尝试对代码作一些修改:

import MyButton from './Button';
class MyComponent extends React.Component {
  render() {
    const button = <MyButton /> return <div>Hello World!</div>; } };复制代码

在组件的render函数中,咱们使用到了另外一个组件MyButton,可是它并无出如今最终返回的DOM结构中。问题来了,当MyComponnet组件渲染至页面上时,Mybutton组件的生命周期函数会开始调用吗?<MyButton />究竟表明了什么?

咱们先回答第二个问题。<MyButton />看上去确实有些奇怪,可是别忘了它是JSX语法。若是你去看babel编译以后的代码就会发现,其实它把<MyButton />转化为函数调用:React.createElement(MyButton, null)。也就是说<XXX />语法,实际上返回的是一个XXX类型的React元素(Element)。React元素说白了就是一个纯粹的object对象,基本由key(id), props(属性), ref, type(元素类型)四个属性组成(children属性包含在props中)。为何要用“纯粹”这个形容词,是由于虽然它和组件有关,可是它并不包含组件的方法,此时此刻,它仅仅是一个包含若干属性的对象。若是你以为这一切看上去都无比熟悉的话,那么你猜对了,元素表明的实际上是虚拟DOM(Virtual DOM)上的节点,是对你在页面上看到的每个DOM节点的描述。

那么咱们能够回答第一个问题了,仅仅是生成一个React元素是不会触发生命周期函数调用的。

当咱们把React元素传递给ReactDOM.render方法,而且告诉它具体在页面上渲染元素的位置以后,它会给咱们返回组件的实例(Instance)。在JS语法中,咱们经过new关键字初始化一个类的实例,而在React中,咱们经过ReactDOM.render方法来初始化一个组件的实例。但通常状况下咱们不会用到这个实例,不过你也能够保留它的引用赋值给一个变量,当测试组件的时候能够派上用场

Default Porps & Default State

若是被问起constructor以后的下一个生命周期函数是什么,绝大部分人会回答componentWillMount。准确来讲应该是getDefaultPropsgetInitialState

而为何大部分人对这两个函数陌生,是由于这两个函数只是在ES2015语法中建立组件时暴露出来,在ES6语法中咱们经过两个赋值语句实现了一样的效果。

好比添加默认属性的getDefaultProps函数在ES6中是经过给组件类添加静态字段defaultProps实现的:

class MyComponent extends React.Component() {
  //...
}
MyComponent.defaultProps = { age: 'unknown' }复制代码

在实际计算属性的过程当中,将传入属性与默认属性进行合并成为最终使用的属性,用伪代码写的意思就是

this.props = Object.assign(defaultProps, passedProps);复制代码

注意知识点要来了,看下面这个组件:

class App extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <div>{this.props.name}</div>
  }
}
App.defaultProps = { name: 'default' };复制代码

我给这个组件设置了一个默认属性name,值为default。那么在

  1. <App name={null} />
  2. <App name={undefined} />
    这两种状况下,this.props.name值会是什么?也就是最终输出会是什么?

正确答案是若是给name传入的值是null,那么最终页面上的输出是空,也就是null会生效;若是传入的是undefined,那么React认为这个值是undefined货真价实的未定义,则会使用默认值,最终页面上的输出是default

而获取默认状态的函数getInitialState在ES6中是经过给this.state赋值实现的

class Person extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  //...
}复制代码

componentWillMount()

componentWillMount函数在第一次render以前被调用,而且只会被调用一次。当组件进入到这个生命周期中时,全部的stateprops已经配置完毕,咱们能够经过this.propsthis.state访问它们,也能够经过setState从新设置状态。总之推荐在这个生命周期函数里进行状态初始化的处理,为下一步render作准备

render()

当一切配置都就绪以后,就可以正式开始渲染组件了。render函数和其余的钩子函数不一样,它会同时在出生和更新阶段被调用。在出生阶段被调用一次,可是在更新阶段会被调用屡次。

不管是编写哪一个阶段的render函数,请牢记一点:保证它的“纯粹”(pure)。怎样才算纯粹?最基本的一点是不要尝试在render里改变组件的状态。由于经过setState引起的状态改变会致使再一次调用render函数进行渲染,而又继续改变状态又继续渲染,致使无限循环下去。若是你这么作了你会在开发模式下收到警告:

Warning: Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

另外一个须要注意的地方是,你也不该该在render中经过ReactDOM.findDOMNode方法访问原生的DOM元素(原生相对于虚拟DOM而言)。由于这么作存在两个风险:

  1. 此时虚拟元素尚未被渲染到页面上,因此你访问的元素并不存在
  2. 由于当前的render即将执行完毕返回新的DOM结构,你访问到的多是旧的数据。

而且若是你真的这么作了,那么你会获得警告:

Warning: App is accessing findDOMNode inside its render(). render() should be a pure function of props and state. It should never access something that requires stale data from the previous render, such as refs. Move this logic to componentDidMount and componentDidUpdate instead.

componentDidMount()

当这个函数被调用时,就意味着能够访问组件的原生DOM了。若是你有经验的话,此时不只仅可以访问当前组件的DOM,还可以访问当前组件孩子组件的原生DOM元素。

你可能会以为全部这一切应当。

在以前讲解每一个周期函数时,都只考虑单个组件的状况。可是当组件包含孩子组件时,孩子组件的钩子函数的调用顺序就须要留意了。

好比有下面这样的树状结构的组件

react element tree
react element tree

在出生阶段时componentWillMountrender的调用顺序是

A -> A.0 -> A.0.0 -> A.0.1 -> A.1 -> A.2.复制代码

这很容易理解,由于当你想渲染父组件时,务必也要当即开始渲染子组件。因此子组件的生命周期开始于父组件以后。

componentDidMount的调用顺序是

A.2 -> A.1 -> A.0.1 -> A.0.0 -> A.0 -> A复制代码

componentDidMount的调用顺序正好是render的反向。这其实也很好理解。若是父组件想要渲染完毕,那么首先它的子组件须要提早渲染完毕,也因此子组件的componentDidMount在父组件以前调用。

正由于咱们能在这个函数中访问原生DOM,因此在这个函数中一般会作一些第三方类库初始化的工具,包括异步加载数据。好比说对c3.js的初始化

import React from 'react';
import ReactDOM from 'react-dom';
import c3 from 'c3';

export default class Chart extends React.Component {

  componentDidMount() {
    this.chart = c3.generate({
      bindto: ReactDOM.findDOMNode(this.refs.chart),
      data: {
        columns: [
          ['data1', 30, 200, 100, 400, 150, 250],
          ['data2', 50, 20, 10, 40, 15, 25]
        ]
      }
    });
  }

  render() {
    return (
      <div ref="chart"></div>
    );
  }
}复制代码

由于可以访问原生DOM的缘故,你可能会在componentDidMount函数中从新对元素的样式进行计算,调整而后生效。所以当即须要对DOM进行从新渲染,此时会使用到forceUpdate方法

本文同时也发布在个人知乎专栏上,也欢迎你们关注

参考

相关文章
相关标签/搜索