我對 React v16.4 生命週期的理解

關於react生命週期的文章,網上一大堆,本人也看了許多,但是覺得大部分人寫的都是照搬其它人的沒有自己獨到的見解,所以決定根據本人的實戰經驗和個人理解再寫一篇React生命週期的文章,由於React目前已更新到16.4版本,所以重點講解React v16.4變化的生命週期,之前的生命週期函式會一帶而過

先總體看下React16的生命週期圖

我對 React v16.4 生命週期的理解
我對 React v16.4 生命週期的理解

React16廢棄的三個生命週期函式

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

注:目前在16版本中 componentWillMount componentWillReceiveProps componentWillUpdate 並未完全刪除這三個生命週期函式,而且添加了 UNSAFE_componentWillMount UNSAFE_componentWillReceiveProps UNSAFE_componentWillUpdate 三個函式,官方計劃在17版本完全刪除這三個函式,只保留UNSAVE_字首的三個函式,目的是為了向下相容,但是對於開發者而言應該儘量避免使用他們,而是使用新增的生命週期函式替代它們

取而代之的是兩個新的生命週期函式

  • static getDerivedStateFromProps
  • getSnapshotBeforeUpdate

我們將React的生命週期分為三個階段,然後詳細講解每個階段具體呼叫了什麼函式,這三個階段是:

  • 掛載階段
  • 更新階段
  • 解除安裝階段

掛載階段

掛載階段,也可以理解為元件的初始化階段,就是將我們的元件插入到DOM中,只會發生一次

這個階段的生命週期函式呼叫如下:

  • constructor
  • getDerivedStateFromProps
  • componentWillMount/UNSAVE_componentWillMount
  • render
  • componentDidMount

constructor

元件建構函式,第一個被執行

如果沒有顯示定義它,我們會擁有一個預設的建構函式

如果顯示定義了建構函式,我們必須在建構函式第一行執行super(props),否則我們無法在建構函式裡拿到this物件,這些都屬於ES6的知識

在建構函式裡面我們一般會做兩件事:

  • 初始化state物件
  • 給自定義方法繫結this
constructor(props) {
    super(props)
    
    this.state = {
      select,
      height: 'atuo',
      externalClass,
      externalClassText
    }

    this.handleChange1 = this.handleChange1.bind(this)
    this.handleChange2 = this.handleChange2.bind(this)
}
複製程式碼

禁止在建構函式中呼叫setState,可以直接給state設定初始值

getDerivedStateFromProps

static getDerivedStateFromProps(nextProps, prevState)

一個靜態方法,所以不能在這個函式裡面使用this,這個函式有兩個引數props和state,分別指接收到的新引數和當前的state物件,這個函式會返回一個物件用來更新當前的state物件,如果不需要更新可以返回null

該函式會在掛載時,接收到新的props,呼叫了setState和forceUpdate時被呼叫

我對 React v16.4 生命週期的理解
我對 React v16.4 生命週期的理解

在React v16.3時只有在掛載時和接收到新的props被呼叫,據說這是官方的失誤,後來修復了

我對 React v16.4 生命週期的理解
我對 React v16.4 生命週期的理解

這個方法就是為了取代之前的 componentWillMount componentWillReceiveProps componentWillUpdate

當我們接收到新的屬性想去修改我們state,可以使用getDerivedStateFromProps

class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
    lastRow: null
  }
  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.currentRow !== prevState.lastRow) {
        return {
            isScrollingDown:
            nextProps.currentRow > prevState.lastRow,
            lastRow: nextProps.currentRow
        }
    }
    return null
  }
}
複製程式碼

componentWillMount/UNSAFE_componentWillMount

在16版本這兩個方法並存,但是在17版本中 componentWillMount 被刪除,只保留 UNSAFE_componentWillMount ,目的是為了做向下相容,對於新的應用,用getDerivedStateFromProps代替它們

由於 componentWillMount/ UNSAFE_componentWillMount 是在render之前呼叫,所以就算在這個方法中呼叫setState也不會觸發重新渲染(re-render)

render

React中最核心的方法,一個元件中必須要有這個方法

返回的型別有以下幾種:

  • 原生的DOM,如div
  • React元件
  • Fragment(片段)
  • Portals(插槽)
  • 字串和數字,被渲染成text節點
  • Boolean和null,不會渲染任何東西

關於Fragment和Portals是React16新增的,如果大家不清楚可以去閱讀官方文件,在這裡就不展開了

render函式是純函式,裡面只做一件事,就是返回需要渲染的東西,不應該包含其它的業務邏輯,如資料請求,對於這些業務邏輯請移到componentDidMount和componentDid Update中

componentDidMount

元件裝載之後呼叫,此時我們可以獲取到DOM節點並操作,比如對canvas,svg的操作,伺服器請求,訂閱都可以寫在這個裡面,但是記得在componentWillUnmount中取消訂閱

componentDidMount() {
    const { progressCanvas, progressSVG } = this

    const canvas = progressCanvas.current
    const ctx = canvas.getContext('2d')
    canvas.width = canvas.getBoundingClientRect().width
    canvas.height = canvas.getBoundingClientRect().height

    const svg = progressSVG.current
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
    rect.setAttribute('x', 0)
    rect.setAttribute('y', 0)
    rect.setAttribute('width', 0)
    rect.setAttribute('height', svg.getBoundingClientRect().height)
    rect.setAttribute('style', 'fill:red')

    const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animate')
    animate.setAttribute('attributeName', 'width')
    animate.setAttribute('from', 0)
    animate.setAttribute('to', svg.getBoundingClientRect().width)
    animate.setAttribute('begin', '0ms')
    animate.setAttribute('dur', '1684ms')
    animate.setAttribute('repeatCount', 'indefinite')
    animate.setAttribute('calcMode', 'linear')
    rect.appendChild(animate)
    svg.appendChild(rect)
    svg.pauseAnimations()

    this.canvas = canvas
    this.svg = svg
    this.ctx = ctx
 }
複製程式碼

在componentDidMount中呼叫setState會觸發一次額外的渲染,多呼叫了一次render函式,但是使用者對此沒有感知,因為它是在瀏覽器重新整理螢幕前執行的,但是我們應該在開發中避免它,因為它會帶來一定的效能問題,我們應該在constructor中初始化我們的state物件,而不應該在componentDidMount呼叫state方法

更新階段

更新階段,當元件的props改變了,或元件內部呼叫了setState或者forceUpdate發生,會發生多次

這個階段的生命週期函式呼叫如下:

  • componentWillReceiveProps/UNSAFE_componentWillReceiveProps
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • componentWillUpdate/UNSAFE_componentWillUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

componentWillReceiveProps/UNSAFE_componentWillReceiveProps

componentWillReceiveProps(nextProps, prevState) UNSAFE_componentWillReceiveProps(nextProps, prevState)

在16版本這兩個方法並存,但是在17版本中 componentWillReceiveProps 被刪除, UNSAFE_componentWillReceiveProps ,目的是為了做向下相容,對於新的應用,用getDerivedStateFromProps代替它們

注意,當我們父元件重新渲染的時候,也會導致我們的子元件呼叫 componentWillReceiveProps / UNSAFE_componentWillReceiveProps ,即使我們的屬性和之前的一樣,所以需要我們在這個方法裡面去進行判斷,如果前後屬性不一致才去呼叫setState

在裝載階段這兩個函式不會被觸發,在元件內部呼叫了setState和forceUpdate也不會觸發這兩個函式

getDerivedStateFromProps

這個方法在裝載階段已經講過了,這裡不再贅述,記住在更新階段,無論我們接收到新的屬性,呼叫了setState還是呼叫了forceUpdate,這個方法都會被呼叫

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState)

有兩個引數nextProps和nextState,表示新的屬性和變化之後的state,返回一個布林值,true表示會觸發重新渲染,false表示不會觸發重新渲染,預設返回true

注意當我們呼叫forceUpdate並不會觸發此方法

因為預設是返回true,也就是隻要接收到新的屬性和呼叫了setState都會觸發重新的渲染,這會帶來一定的效能問題,所以我們需要將this.props與nextProps以及this.state與nextState進行比較來決定是否返回false,來減少重新渲染

但是官方提倡我們使用PureComponent來減少重新渲染的次數而不是手工編寫shouldComponentUpdate程式碼,具體該怎麼選擇,全憑開發者自己選擇

在未來的版本,shouldComponentUpdate返回false,仍然可能導致元件重新的渲染,這是官方自己說的

Currently, if shouldComponentUpdate() returns false, then UNSAFE_componentWillUpdate(), render(), and componentDidUpdate() will not be invoked. In the future React may treat shouldComponentUpdate() as a hint rather than a strict directive, and returning false may still result in a re-rendering of the component.

componentWillUpdate/UNSAFE_componentWillUpdate

componentWillUpdate(nextProps, nextState)  UNSAFE_componentWillUpdate(nextProps, nextState)

在16版本這兩個方法並存,但是在17版本中 componentWillUpdate 被刪除, UNSAFE_componentWillUpdate ,目的是為了做向下相容

在這個方法裡,你不能呼叫setState,因為能走到這個方法,說明shouldComponentUpdate返回true,此時下一個state狀態已經被確定,馬上就要執行render重新渲染了,否則會導致整個生命週期混亂,在這裡也不能請求一些網路資料,因為在非同步渲染中,可能會導致網路請求多次,引起一些效能問題,

如果你在這個方法裡儲存了滾動位置,也是不準確的,還是因為非同步渲染的問題,如果你非要獲取滾動位置的話,請在getSnapshotBeforeUpdate呼叫

render

更新階段也會觸發,裝載階段已經講過了,不再贅述

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState)

這個方法在render之後,componentDidUpdate之前呼叫,有兩個引數prevProps和prevState,表示之前的屬性和之前的state,這個函式有一個返回值,會作為第三個引數傳給componentDidUpdate,如果你不想要返回值,請返回null,不寫的話控制檯會有警告

我對 React v16.4 生命週期的理解
我對 React v16.4 生命週期的理解

還有這個方法一定要和componentDidUpdate一起使用,否則控制檯也會有警告

我對 React v16.4 生命週期的理解
我對 React v16.4 生命週期的理解

前面說過這個方法時用來代替 componentWillUpdate / UNSAVE_componentWillUpdate ,下面舉個例子說明下:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}
複製程式碼

componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot)

該方法在getSnapshotBeforeUpdate方法之後被呼叫,有三個引數prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三個引數是getSnapshotBeforeUpdate返回的

在這個函式裡我們可以操作DOM,和發起伺服器請求,還可以setState,但是注意一定要用if語句控制,否則會導致無限迴圈

解除安裝階段

解除安裝階段,當我們的元件被解除安裝或者銷燬了

這個階段的生命週期函式只有一個:

  • componentWillUnmount

componentWillUnmount

當我們的元件被解除安裝或者銷燬了就會呼叫,我們可以在這個函式裡去清除一些定時器,取消網路請求,清理無效的DOM元素等垃圾清理工作

注意不要在這個函式裡去呼叫setState,因為元件不會重新渲染了