> For the complete documentation index, see [llms.txt](https://discountry.gitbook.io/learn-react-from-scratch/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://discountry.gitbook.io/learn-react-from-scratch/react/24-zu-jian-sheng-ming-zhou-qi.md).

# 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方法将组件卸载，这其中进行的就是卸载流程。

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

```jsx
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非常有帮助
