2-4-组件生命周期

  • React是如何渲染组件的

  • 渲染组件时会触发的生命周期方法

    • 挂载流程

    • 更新流程

    • 卸载流程

React是如何渲染组件的

我们就按照平时书写React代码的顺序来理清React把组件代码渲染到最终的真实DOM中的流程。

一般来讲,我们都会先定义一个组件。我们想要在页面中看到这个组件的渲染结果的化,就需要以JSX的形式将组件传入ReactDOM.render方法的第一个参数,我们要理解,这里的JSX经过React内部的转译,其实是一个React.createElement创建React元素的方法。render方法获取到React元素之后会将它实例化,之后它会根据实例化的React元素创建出真实的DOM元素,再根据第二个参数的指向,将创建好的元素插入到目标DOM容器当中。

为了加深理解,还是稍微来了解一下React本身运行流程。

在新版本的React当中,React的底层被重写了。React换上了一个新的引擎,这个引擎叫做React Fiber.React Fiber 作用的也即是React最核心的功能,它将React应用界面更新的过程分为了两个主要的部分:

  • 调度过程

  • 执行过程

前面还是一样,React对虚拟DOM进行Diff操作,对比应用前后改变的部分,之后进入调度阶段,React Fiber会计算出最优化的调度方法,以防止我们更新DOM是发生卡顿阻塞等状况,然后进入执行过程提交调度计算的结果,最终由ReactDOM负责将页面中的内容渲染到页面当中。

在调度过程中,有4个生命周期函数会被触发:

  • componentWillMount

  • componentWillReceiveProps

  • shouldComponentUpdate

  • componentWillUpdate

在执行过程中,有3个生命周期函数会被触发:

  • componentDidMount

  • componentDidUpdate

  • componentWillUnmount

React组件生命周期方法

React为了方便我们更好地控制自己的应用,提供了许多预置的生命周期方法。就好像我们上面介绍的挂载流程,这些固定的生命周期方法分别会在组件的挂载流程、更新流程、卸载流程中触发。

假如有同学实在是不理解上一个部分介绍的渲染原理也没有关系,在学习下面的内容之前,我们只需要明确,React的初次render会进行挂载流程,挂载之后的后续渲染进行的都是更新流程,最后,我们也可以通过ReactDOM.unmountComponentAtNode方法将组件卸载,这其中进行的就是卸载流程。

如果只是一直讲解概念,感觉会很抽象不好理解,所以我们不如来亲眼看一看这三个流程是如何运行,如何触发生命周期函数的:

class Number extends React.Component {
  constructor(props) {
    super(props)
    console.log('%cconstructor'+'%c 子组件已构造', 'font-weight:bold', 'color: blue')
  }

   componentWillMount() {
      console.group("%c React 挂载", 'color: #00d8ff')
      console.log('%ccomponentWillMount'+'%c 组件即将挂载', 'font-weight:bold', '')
   }

   componentDidMount() {
      console.log('%ccomponentDidMount'+'%c 组件已挂载', 'font-weight:bold', '')
      console.groupEnd();
   }

   componentWillReceiveProps(newProps) { 
      console.group("%c React Updating", 'color: green')
      console.log('%ccomponentWillReceiveProps'+'%c 组件即将接收props', 'font-weight:bold', '')
      console.log('newProps', newProps.counter)
      console.log('this.props', this.props.counter)
   }

   shouldComponentUpdate(newProps, newState) {
      const result = true
      console.info('%cshouldComponentUpdate'+'%c 返回判断是否要更新组件' +`%c ${result}`, 'font-weight:bold', 'color: #ff3c41', 'font-weight:bold;color: #236fd4')
      if (!result) console.groupEnd()
      return result;
   }

   componentWillUpdate(nextProps, nextState) {
      console.log('%ccomponentWillUpdate'+'%c 组件即将更新', 'font-weight:bold', '')
      console.log('nextProps', nextProps.counter)
      console.log('this.props', this.props.counter)
   }

   componentDidUpdate(prevProps, prevState) {
      console.log('%ccomponentDidUpdate' + '%c 组件已更新', 'font-weight:bold', '')
      console.log('prevProps', prevProps.counter)
      console.log('this.props', this.props.counter)
      console.groupEnd();
   }

   componentWillUnmount() {
      console.group("%c React Unmounting", 'color: brown')
      console.log('%ccomponentWillUnmount'+'%c 组件即将卸载','font-weight:bold' , 'color: gray')
      console.groupEnd();
   }

   render() {
      console.log('%crender'+'%c 组件渲染中...', 'font-weight:bold', '')
      console.log('this.props', this.props.counter)
      return <p>{ this.props.counter }</p>
   }
}

class Counter extends React.Component {
  constructor(props) {
    super(props)
    console.log('%cconstructor'+'%c 父组件已构造', 'font-weight:bold', 'color: #ae63e4')
    this.state = {
      counter: 0
    }
    console.log('this.state', this.state.counter)
  }

  addOne() {
    console.log('%caddOne()'+'%c 事件处理函数触发', 'font-weight:bold', '')
    console.log('prevState', this.state.counter)
    this.setState((prevState) =>({
      counter: prevState.counter + 1
    }))
  }

  unMount() {
    ReactDOM.unmountComponentAtNode(document.getElementById('root'));
  }

  update() {
    this.forceUpdate()
  }

  render() {
    console.log('%crender'+'%c 父组件渲染中...', 'font-weight:bold', '')
    console.log('nextState', this.state.counter)
    return (
      <div>
        <Number counter={this.state.counter} />
        <button
          onClick={() => this.addOne()}>
          增加
        </button>
        <button
          onClick={() => this.update()}>
          强制更新
        </button>
        <button
          onClick={() => this.unMount()}>
          卸载
        </button>
      </div>
    )
  }
}

const render = () => ReactDOM.render(
  <Counter />,
  document.getElementById('root')
)

document.getElementById('render').addEventListener('click',render)

这里我们定义了两个组件,父组件会传递state到子组件当中,顺便state的变化和props的传递流程我们也可以在这个示例中看到。子组件的每个生命周期函数中我们都在控制台输出了相关的信息,这样在它被触发的时候,我们在控制台中就能够观察到。

挂载流程

首先是组件初次渲染的挂载流程,我们通过一个按钮绑定渲染函数来手动触发组件的渲染。

初次挂载组件时,我们定义的组件类首先会被构造声明。在渲染之前会触发componentWillMount,渲染完成会触发componentDidMount这两个生命周期函数。

componentWillMount方法会在render之前被触发,只会在组件被渲染出来之前触发一次,如果你是使用ES6的Class声明组件的话,时机和作用几乎与constructor相同,在componentWillMount方法里对state定义或改变不会触发重新渲染。

componentDidMount在初次渲染完成之后被触发,也只会触发一次,在这个方法里你已经可以访问渲染出的DOM元素了。官方推荐在这个函数中进行一些例如ajax请求的操作,所以它也是我们最经常使用的生命周期函数。

更新流程

我们是在子组件中观察生命周期函数运行的,之前已经提到过了,这个示例中父组件会通过props向子组件传递state中的计数值,所以我们首先观察到的被触发的生命周期函数叫做componentWillReceiveProps这是在我们调用this.setState()方法之后,子组件收到新的props之后会触发的函数。值得提醒的一点是,正如这个函数命名中的Will,更新流程运行到这一步,组件的props还没有改变,我们可以通过nextProps获取到新的props,但如果我们试着查看this.props会发现它还是之前的值。

接下来被触发的shouldComponentUpdate是一个很特殊的函数,它默认会返回ture,但假如这个函数返回false的话,更新流程就会被跳过,render也不会继续被触发。假如你想要提升你应用的性能,可以在这个函数中自定义一些判断来跳过不需要被更新的组件。

然后通过实验我们也可以看到,组件在初次渲染流程或者使用forceUpdate方法时是不会触发这个函数的。

接下来的两个生命周期函数componentWillUpdate和componentDidUpdate分别会在render的前后被触发。

需要注意的是在componentWillUpdate无法调用this.setState()你可以理解为更新流程到这一步想要再更改state已经晚了。如果有需要你可以在之前的componentWillReceiveProps中更新state,React会把改变合并到一个更新流程里进行。

而componentDidUpdate则是另一个比较适合我们发起ajax请求的地方,在这个方法里我们还可以比较前后的props变化,再决定是否发起网络请求。一个比较实际的使用场景是保存用户输入到服务器,用户可能会来来回回修改输入的内容,但假如我们判断在修改前后数据最终没有改变,就没有必要发起不必要的网络请求了。

卸载流程

组件既然可以被挂载,同样也可以被卸载,我们可以通过一个名为ReactDOM.unmountComponentAtNode的方法来卸载已经挂载的React组件。在卸载流程中,名为componentWillUnmount的函数会被触发。在组件挂载之后,我们可能定义了一些计时器、绑定了事件监听函数等等,在卸载流程的生命周期函数中,也是我们解绑这些函数的合适位置。

实例

接下来呢,我们一起来实现一个小的例子,掌握一下生命周期函数的具体使用方法。

我们要写的是一个从Reddit异步获取帖子内容的小应用。

我们还是沿用之前的展示组件与容器组件的划分方法,一共要写两个组件。

首先我们来写应用的展示组件。

我们通过 const 关键字声明,使用箭头函数定义一个简单的名为RedditList的组件。

一般我们的组件包含多层嵌套的JSX的时候,我们都会习惯在最外面套上一层小括号,因为浏览器在某些情况下可能会自动给换行的代码加上分号,在最外面套上小括号可以消除这一影响。

首先我们定义组件的标题,也就是帖子板块的名称,在这里我们使用字符串模板的方法,就可以省去一些运算符拼接字符串。

之后我们要使用JSX来渲染一个列表。在之前的课程中我们介绍过,JSX当中是可以使用JS的表达式的。一般在渲染列表的时候,我们会拿到一个数组数据,之后通过数组的map方法来渲染出列表的每一项。在这里我们就直接使用JS当中数组的map方法,在传入的函数当中,我们还是使用箭头函数的语法,数组中的每一项对应也返回列表中的一项。

需要特别强调的是,JSX当中渲染列表项的时候,每一项必须带有key属性作为识别列表中某一项的标识。React在渲染组件时需要通过key属性的值来判定列表中的每一项内容。

接下来我们来编写容器组件 RedditFetch . 我们通过类定义的方法来声明它。在它的构造函数里,我们先初始化这个应用的state,在刚才的展示组件当中,我们使用的是一个数组数据,因此在初始化的时候,我们也要把对应的状态数据初始化为数组。

接下来我们定义一个从服务器异步获取数据的方法fetchFromApi,在这里我们使用的是一个名为axios的专门用来发起异步请求的库,当然你也可以用jQuery或者原生的JS方法,你可以选择你自己熟悉的实现方式。在请求成功之后呢,重新设置我们应用的state,获取到posts帖子的数据。

之后就是最关键的部分啦。在这里我们要使用到两个生命周期函数,第一个是componentDidMount,我们刚才介绍过,这里是最适合发起异步服务器请求的地方。因此在其中我们调用刚才定义好的fetchFromApi方法。

再然后是componentWillUnMount,这个生命周期函数是在组件的卸载过程中被触发的,一般在这个生命周期函数中,我们都是做一些解除取消的方法,比方说在这里,我们可以取消掉向服务器发起的请求。

最后在组件的render方法中,调用刚才定义的展示组件RedditList,传入对应的props值,板块名称以及帖子数据,就都OK啦。

最后在ReactDOM的渲染方法中,我们为整个应用传入板块的名称,效果就是我们现在看到的。

总结

  • React组件渲染包含三个流程:挂载流程、更新流程、卸载流程

  • 各个生命周期函数会在特定的时刻触发并适用于不同的使用场景

  • 通过使用生命周期函数我们可以对应用进行更精准的控制

  • 如果你需要发起网络请求,将其安排在合适的生命周期函数中是值得推荐的做法

  • 了解掌握React组件渲染的流程和原理对我们更深入掌握React非常有帮助

Last updated