组件生命周期

React 是如何渲染组件的?

一般来讲,我们都会先定义一个组件,我们想要在页面当中看到这个组件渲染的结果的话,需要用 JSX 的形式把组件传入到 ReactDOM.render 方法中作为第一个参数。JSX 通过 React 转化其实是一个 react.createElement 的方法。render 方法获取到 react 元素中会将它实例化,根据 React 实例的元素创建出真实DOM。再更具第二个参数的指向,讲创建好的元素插入到目标DOM容器当中。

新版本的React当中,React的底层被重写,换上新的引擎,React Fiber。它将 React 更新应用界面分成了 2 个主要的部分调度过程执行过程

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

  • componentWillMount
  • conponentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

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

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

React 为了方便我们控制自己的引用提供了许多预知的生命周期方法,就想以上的生命周期方法,会在组件挂载流程、更新流程、卸载流程中触发。React初次渲染时,属于挂载流程,后续更新时的都是更新流程,最后我们还可以通过 ReactDOM.unmountComponentAtNode 将组件卸载,其中进行的就是卸载流程。

举个栗子 。这里我们定义2个组件,父组件会传递 state 到子组件当中,顺便 state 的变化和 props 的传递流程我们也可以从这个 中看到。子组件每一个生命周期函数中,都在控制台输出相关的信息。

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

class Number extends React.Component {
  constructor(props) {
    super(props)
    console.log('constructor 子组件已构造')
  }

  componentWillMount() {
    console.group("React 挂载")
    console.log('componentWillMount 组件即将挂载')
  }

  componentDidMount() {
    console.log('componentDidMount 组件已挂载')
    console.groupEnd();
  }

  componentWillReceiveProps(newProps) {
    console.group("React 更新")
    console.log('componentWillReceiveProps 组件即将接收props')
    console.log('newProps', newProps.counter)
    console.log('this.props', this.props.counter)
  }

  shouldComponentUpdate(newProps, newState) {
    const result = true
    console.info('shouldComponentUpdate 返回判断是否要更新组件')
    if (!result) console.groupEnd()
    return result;
  }

  componentWillUpdate(nextProps, nextState) {
    console.log('componentWillUpdate 组件即将更新')
    console.log('nextProps', nextProps.counter)
    console.log('this.props', this.props.counter)
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('componentDidUpdate 组件已更新')
    console.log('prevProps', prevProps.counter)
    console.log('this.props', this.props.counter)
    console.groupEnd();
  }

  componentWillUnmount() {
    console.group("React 卸载")
    console.log('componentWillUnmount 组件即将卸载')
    console.groupEnd();
  }

  render() {
    console.log('组件渲染中...')
    console.log('this.props', this.props.counter)
    return <p>{ this.props.counter }</p>
  }
}

class Counter extends React.Component {
  constructor(props) {
    super(props)
    console.log('constructor 父组件已构造')
    this.state = {
      counter: 0
    }
  }

  componentWillMount() {
    console.log('this.state', this.state.counter)
  }

  shouldComponentUpdate(newProps, newState) {
    const result = true
    console.info('shouldComponentUpdate 返回判断是否要更新父组件')
    return result;
  }

  addOne() {
    console.log('addOne() 事件处理函数触发')
    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('render 父组件渲染中...')
    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>
    )
  }
}

ReactDOM.render(
  <Counter />,
  document.getElementById('root')
)
  1. 首先是组件初次渲染的挂载流程,我们通过一个按钮,绑定一个渲染函数来主动触发组件的渲染。初次挂载组件时,我们构建的组件类会被挂载声明,在渲染之前会触发componentWillMount,在渲染之后会触发componentDidMount这2个生命周期函数。

  2. componentWillMount会在渲染之前被触发,并且只会被组件被渲染之前触发一次。如果使用ES6的class声明组件的话,时机和作用几乎和constructor相同。在componentWillMount对state的定义或者改变,是不会触发新的渲染的。

  3. componentDidMount会在渲染完成之后被触发,同样自会触发一次。在这个方法中,已经可以访问到渲染到页面的DOM元素了,所以官方推荐在这个方法中可以进行一些类似于AJAX请求的操作,是我们最经常使用的生命周期函数。

  4. 父组件会向子组件更新props的数值,这时候会触发componentWillReceiveProps。组件在运行这一流程,props还没有被改变。然后就会就会触发shouldComponentUpdate,默认返回true,如果返回false的话,更新流程会被跳过,渲染也不会继续被触发。假如要提升应用的性能,可以在这个函数中进行自定义的判断来跳过不需要被更新的组件。组件在初次渲染时,和forceUpdate时,是不会触发这个函数的。

  5. componentWillUpdatecomponentDidUpdate会在更新渲染的前后触发。在componentWillUpdate里无法调用this.setState,可以理解为更新流程到这一步想要更改state已经晚了,如果有需要可以在componentWillReceiveProps中更新state,React会把改变合并到同一个更新流程里面执行。而componentDidUpdate是另一个适合我们发起AJAX请求的地方,在这里我们还可以比较前后的props变化,再决定是否发起网络请求。比较实用的场景就是保存用户的输入到服务器,用户可能会来来回回修改内容,如果我们判断修改前后数据最终没有被改变,那就没有必要发起不必要的网络请求了。

  6. 通过ReactDOM.unmountComponentAtNode来卸载已经挂载了的React组件。在卸载流程的componentWillUnmount是我们解绑事件最合适的位置。

>> constructor 父组件已构造
>> this.state 0
>> render 父组件渲染中...
>> nextState 0

>>constructor 子组件已构造
>>componentWillMount 组件即将挂载
>>render 组件渲染中...
>>this.props 0
>>componentDidMount 组件已挂载 

>>addOne 事件处理函数触发
>>prevState 0
>>shouldComponentUpdate 返回判断是否要更新父组件
>>render 父组件渲染中...
>>nextState 1

>>componentWillReceiveProps 组件即将接受Props
>>nextProps 1
>>this.props 0
>>shouldComponentUpdate 返回判断是否要更新组件
>>componentWillUpdate 组件即将更新
>>nextProps 1
>>this.props 0
>>render 组件渲染中...
>>this.props 1
>>componentDidUpdate 组件已更新
>>preProps 0
>>this.props 1

>> componentWillUnmount 组件即将卸载

例子:React异步获取文章

通过 const 的关键字声明,使用 arrowFunction 定义一个简单的 RedditList 展示组件。一般组件包含多层嵌套的JSX的时候,都会加上一个小括号,因为浏览器在某些情况下可能会对换行的代码加上分号。 JSX当中时可以使用js表达式的,一般在渲染列表中都会拿到一个数组数据,通过map的方法渲染出列表中的每一项。JSX在渲染列表项的时候,每一项都必须带有key属性,作为识别列表中某一项的标识,React在渲染列表的时候会通过key来判断每一个的内容。

使用类定义的方法来定义一个 RedditFetch 容器组件。首先把posts初始化为数组,定义一个从服务器获取数据的方法 fetchFromApi,在请求成功之后重新设置 state,获取到帖子的数据。在 componentDidMount 中调取 fetchFromApi,在 componentWillUnmount 中解除向服务器发起的请求。

const RedditList = props => (
  <div>
    <h1>{`/r/${props.subreddit}`}</h1>
    <ul>
      {props.posts.map(post =>
        <li key={post.id}>
          <a href={post.url}>{post.title}</a>  
        </li>
      )}
    </ul>
  </div>
)

class RedditFetch extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      posts: []
    }
  }
  
  fetchFromApi() {
    this.serverRequest = axios.get(`https://www.reddit.com/r/${this.props.subreddit}.json`)
      .then(res => {
        const posts = res.data.data.children.map(obj => obj.data)
        this.setState({
          posts
        })
      })
    }
  
    componentDidMount() {
      this.fetchFromApi()
    }
  
    componentWillUnmount() {
      this.serverRequest.abort()
    }
  
    render() {
      return <RedditList subreddit={this.props.subreddit} posts={this.state.posts} />
    }
}

ReactDOM.render(<RedditFetch subreddit="reactjs" />,document.getElementById('root'))

小结

  • React 组件渲染包括三个流程:挂载流程、更新流程、卸载流程。
  • 各个生命周期函数会在特定的时候触发并适用于不同的使用场景。
  • 通过使用生命周期函数我们可以对应用进行更精准的控制。
  • 如果你需要发起网络请求,将其安排在适合的生命函数周期中。