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

網上有不少關於 React 生命週期的文章,我也看了很多,爲了梳理並加深我對此的理解,因此決定寫這篇文章。本文主要梳理目前最新的 V16.4 的生命週期函數。如今 React 最新版本是 16.13,可是生命週期最新版本是 16.4,以後版本的生命週期沒有過改動了,本文不涉及 Hooks。html

先上示意圖:前端

React 生命週期示意圖

廢棄三個舊的生命週期函數

React 在 V16.3 版本中,爲下面三個生命週期函數加上了 UNSAFEreact

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

標題中的廢棄不是指真的廢棄,只是不建議繼續使用,並表示在 V17.0 版本中正式刪除。先來講說 React 爲何要這麼作。算法

主要是這些生命週期方法常常被誤用和濫用。而且在 React V16.0 以前,React 是同步渲染的,而在 V16.0 以後 React 更新了其渲染機制,是經過異步的方式進行渲染的,在 render 函數以前的全部函數都有可能被執行屢次。數組

長期以來,原有的生命週期函數老是會誘惑開發者在 render 以前的生命週期函數中作一些動做,如今這些動做還放在這些函數中的話,有可能會被調用屢次,這確定不是咱們想要的結果。瀏覽器

廢棄 UNSAFE_componentWillMount 的緣由

有一個常見的問題,有人問爲何不在 UNSAFE_componentWillMount 中寫 AJAX 獲取數據的功能,他們的觀點是,UNSAFE_componentWillMountrender 以前執行,早一點執行早獲得結果。可是要知道,在 UNSAFE_componentWillMount 中發起 AJAX 請求,無論多快獲得結果也趕不上首次 render,數據都是要在 render 後才能到達。安全

並且 UNSAFE_componentWillMount 在服務器端渲染也會被調用到(此方法是服務端渲染惟一會調用的生命週期函數。),你確定不但願 AJAX 請求被執行屢次,因此這樣的 IO 操做放在 componentDidMount 中更合適。性能優化

尤爲是在 Fiber 啓用了異步渲染以後,更沒有理由在 UNSAFE_componentWillMount 中進行 AJAX 請求了,由於 UNSAFE_componentWillMount 可能會被調用屢次,誰也不會但願無謂地屢次調用 AJAX 吧。服務器

還有人會將事件監聽器(或訂閱)添加到 UNSAFE_componentWillMount 中,但這可能致使服務器渲染(永遠不會調用 componentWillUnmount)和異步渲染(在渲染完成以前可能被中斷,致使不調用 componentWillUnmount)的內存泄漏。微信

人們一般認爲 UNSAFE_componentWillMountcomponentWillUnmount 是成對出現的,但這並不能保證。只有調用了 componentDidMount 以後,React 才能保證稍後調用 componentWillUnmount 進行清理。所以,添加監聽器/訂閱的推薦方法是使用 componentDidMount 生命週期。

廢棄 UNSAFE_componentWillReceiveProps 的緣由

有時候組件在 props 發生變化時會產生反作用。與 UNSAFE_componentWillUpdate 相似,UNSAFE_componentWillReceiveProps 可能在一次更新中被屢次調用。所以,避免在此方法中產生反作用很是重要。相反,應該使用 componentDidUpdate,由於它保證每次更新只調用一次。

UNSAFE_componentWillReceiveProps 是考慮到由於父組件引起渲染可能要根據 props 更新 state 的須要而設立的。新的 getDerivedStateFromProps 實際上與 componentDidUpdate 一塊兒取代了之前的 UNSAFE_componentWillReceiveProps 函數。

廢棄 UNSAFE_componentWillUpdate 的緣由

有些人使用 UNSAFE_componentWillUpdate 是出於一種錯誤的擔憂,即當 componentDidUpdate 觸發時,更新其餘組件的 state 已經」太晚」了。事實並不是如此。React 可確保在用戶看到更新的 UI 以前,刷新在 componentDidMountcomponentDidUpdate 期間發生的任何 setState 調用。

一般,最好避免這樣的級聯更新。固然在某些狀況下,這些更新也是必需的(例如:若是你須要在測量渲染的 DOM 元素後,定位工具的提示)。無論怎樣,在異步模式下使用 UNSAFE_componentWillUpdate 都是不安全的,由於外部回調可能會在一次更新中被屢次調用。相反,應該使用 componentDidUpdate 生命週期,由於它保證每次更新只調用一次。

大多數開發者使用 UNSAFE_componentWillUpdate 的場景是配合 componentDidUpdate,分別獲取 rerender 先後的視圖狀態,進行必要的處理。但隨着 React 新的 suspensetime slicing、異步渲染等機制的到來,render 過程能夠被分割成屢次完成,還能夠被暫停甚至回溯,這致使 UNSAFE_componentWillUpdatecomponentDidUpdate 執行先後可能會間隔很長時間,足夠使用戶進行交互操做更改當前組件的狀態,這樣可能會致使難以追蹤的 BUG。

React 新增的 getSnapshotBeforeUpdate 方法就是爲了解決上述問題,由於 getSnapshotBeforeUpdate 方法是在 UNSAFE_componentWillUpdate 後(若是存在的話),在 React 真正更改 DOM 前調用的,它獲取到組件狀態信息更加可靠。

除此以外,getSnapshotBeforeUpdate 還有一個十分明顯的好處:它調用的結果會做爲第三個參數傳入 componentDidUpdate,避免了 UNSAFE_componentWillUpdate 和 componentDidUpdate 配合使用時將組件臨時的狀態數據存在組件實例上浪費內存,getSnapshotBeforeUpdate 返回的數據在 componentDidUpdate 中用完即被銷燬,效率更高。

更多問題詳見:

新增兩個生命週期函數

React V16.3 中在廢棄(這裏的廢棄不是指真的廢棄,只是不建議繼續使用,並表示在 V17.0 版本中正式刪除)三個舊的生命週期函數的同時,React 還新增了兩個生命週期函數:

  • static getDerivedStateFromProps
  • getSnapshotBeforeUpdate

在 React V16.3 版本中加入的 static getDerivedStateFromProps 生命週期函數存在一個問題,就是在生命週期的更新階段只有在 props 發生變化的時候纔會調用 static getDerivedStateFromProps,而在調用了 setStateforceUpdate 時則不會。

React 官方也發現了這個問題,並在 React V16.4 版本中進行了修復。也就是說在更新階段中,接收到新的 props,調用了 setStateforceUpdate 時都會調用 static getDerivedStateFromProps。具體在下面講到這個函數的時候有詳細說明。

React 生命週期梳理

React 生命週期主要分爲三個階段:

  • 掛載階段
  • 更新階段
  • 卸載階段

掛載階段

掛載階段也能夠理解爲初始化階段,也就是把咱們的組件插入到 DOM 中。這個階段的過程以下:

  • constructor
  • getDerivedStateFromProps
  • UNSAVE_componentWillMount
  • render
  • (React Updates DOM and refs)
  • componentDidMount

constructor

組件的構造函數,第一個被執行。若是在組件中沒有顯示定義它,則會擁有一個默認的構造函數。若是咱們顯示定義構造函數,則必須在構造函數第一行執行 super(props),不然咱們沒法在構造函數裏拿到 this,這些都屬於 ES6 的知識。

在構造函數中,咱們通常會作兩件事:

  • 初始化 state
  • 對自定義方法進行 this 的綁定
constructor(props) {
    super(props);

    this.state = {
      width,
      height: 'atuo',
    }

    this.handleChange1 = this.handleChange1.bind(this);
    this.handleChange2 = this.handleChange2.bind(this);
}
複製代碼

getDerivedStateFromProps

使用方式:

//static getDerivedStateFromProps(nextProps, prevState)

class Example extends React.Component {
  static getDerivedStateFromProps(props, state) {
    //根據 nextProps 和 prevState 計算出預期的狀態改變,返回結果會被送給 setState
    // ...
  }
}
複製代碼

新的 getDerivedStateFromProps 是一個靜態函數,因此不能在這函數裏使用 this,簡單來講就是一個純函數。也代表了 React 團隊想經過這種方式防止開發者濫用這個生命週期函數。每當父組件引起當前組件的渲染過程時,getDerivedStateFromProps 會被調用,這樣咱們有一個機會能夠根據新的 props 和當前的 state 來調整新的 state

這個函數會返回一個對象用來更新當前的 state,若是不須要更新能夠返回 null。這個生命週期函數用得比較少,主要用於在從新渲染期間手動對滾動位置進行設置等場景中。該函數會在掛載時,在更新時接收到新的 props,調用了 setStateforceUpdate 時被調用。

getDerivedStateFromProps

新的 getDerivedStateFromProps 實際上與 componentDidUpdate 一塊兒取代了之前的 UNSAFE_componentWillReceiveProps 函數。UNSAFE_componentWillReceiveProps 也是考慮到由於父組件引起渲染可能要根據 props 更新 state 的須要而設立的。

UNSAVE_componentWillMount

UNSAFE_componentWillMount() 在掛載以前被調用。它在 render() 以前調用,所以在此方法中同步調用 setState() 不會觸發額外渲染。一般,咱們建議使用 constructor() 來初始化 state。避免在此方法中引入任何反作用或訂閱。如遇此種狀況,請改用 componentDidMount()

此方法是服務端渲染惟一會調用的生命週期函數。UNSAFE_componentWillMount() 經常使用於當支持服務器渲染時,須要同步獲取數據的場景。

render

這是 React 中最核心的方法,class 組件中惟一必須實現的方法。

render 被調用時,它會檢查 this.propsthis.state 的變化並返回如下類型之一:

  • 原生的 DOM,如 div
  • React 組件
  • 數組或 Fragment
  • Portals(插槽)
  • 字符串和數字,被渲染成文本節點
  • Boolean 或 null,不會渲染任何東西

render() 函數應該是一個純函數,裏面只作一件事,就是返回須要渲染的東西,不該該包含其它的業務邏輯,如數據請求,對於這些業務邏輯請移到 componentDidMountcomponentDidUpdate 中。

componentDidMount

componentDidMount() 會在組件掛載後(插入 DOM 樹中)當即調用。依賴於 DOM 節點的初始化應該放在這裏。如需經過網絡請求獲取數據,此處是實例化請求的好地方。這個方法是比較適合添加訂閱的地方。若是添加了訂閱,請不要忘記在 componentWillUnmount() 裏取消訂閱

你能夠在 componentDidMount() 裏直接調用 setState()。它將觸發額外渲染,但此渲染會發生在瀏覽器更新屏幕以前。如此保證了即便在 render() 兩次調用的狀況下,用戶也不會看到中間狀態。

請謹慎使用該模式,由於它會致使性能問題。一般,你應該在 constructor() 中初始化 state。若是你的渲染依賴於 DOM 節點的大小或位置,好比實現 modalstooltips 等狀況下,你可使用此方式處理

更新階段

更新階段是指當組件的 props 發生了改變,或組件內部調用了 setState 或者發生了 forceUpdate,則進行更新。

這個階段的過程以下:

  • UNSAFE_componentWillReceiveProps
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • UNSAFE_componentWillUpdate
  • render
  • getSnapshotBeforeUpdate
  • (React Updates DOM and refs)
  • componentDidUpdate

UNSAFE_componentWillReceiveProps

UNSAFE_componentWillReceiveProps 是考慮到由於父組件引起渲染可能要根據 props 更新 state 的須要而設立的。UNSAFE_componentWillReceiveProps 會在已掛載的組件接收新的 props 以前被調用。若是你須要更新狀態以響應 prop 更改(例如,重置它),你能夠比較 this.propsnextProps 並在此方法中使用 this.setState() 執行 state 轉換。

若是父組件致使組件從新渲染,即便 props 沒有更改,也會調用此方法。若是隻想處理更改,請確保進行當前值與變動值的比較。在掛載過程當中,React 不會針對初始 props 調用 UNSAFE_componentWillReceiveProps()。組件只會在組件的 props 更新時調用此方法。調用 this.setState() 一般不會觸發 UNSAFE_componentWillReceiveProps()

getDerivedStateFromProps

這個方法在掛載階段已經講過了,這裏再也不贅述。記住該函數會在掛載時,在更新時接收到新的 props,調用了 setStateforceUpdate 時被調用。它與 componentDidUpdate 一塊兒取代了之前的 UNSAFE_componentWillReceiveProps 函數。

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState) {
  //...
}
複製代碼

它有兩個參數,根據此函數的返回值來判斷是否進行從新渲染,true 表示從新渲染,false 表示不從新渲染,默認返回 true。注意,首次渲染或者當咱們調用 forceUpdate 時並不會觸發此方法。此方法僅用於性能優化。

由於默認是返回 true,也就是隻要接收到新的屬性和調用了 setState 都會觸發從新的渲染,這會帶來必定的性能問題,因此咱們須要將 this.propsnextProps 以及 this.statenextState 進行比較來決定是否返回 false,來減小從新渲染,以優化性能。請注意,返回 false 並不會阻止子組件在 state 更改時從新渲染。

可是官方提倡咱們使用內置的 PureComponent 來減小從新渲染的次數,而不是手動編寫 shouldComponentUpdate 代碼。PureComponent 內部實現了對 props 和 state 進行淺層比較。

若是 shouldComponentUpdate() 返回 false,則不會調用 UNSAFE_componentWillUpdate()render()componentDidUpdate()。官方說在後續版本,React 可能會將 shouldComponentUpdate 視爲提示而不是嚴格的指令,而且,當返回 false 時,仍可能致使組件從新渲染。

UNSAFE_componentWillUpdate

當組件收到新的 propsstate 時,會在渲染以前調用 UNSAFE_componentWillUpdate()。使用此做爲在更新發生以前執行準備更新的機會。初始渲染不會調用此方法。可是你不能此方法中調用 this.setState()。在 UNSAFE_componentWillUpdate() 返回以前,你也不該該執行任何其餘操做(例如,dispatch Redux 的 action)觸發對 React 組件的更新。

一般,此方法能夠替換爲 componentDidUpdate()。若是你在此方法中讀取 DOM 信息(例如,爲了保存滾動位置),則能夠將此邏輯移至 getSnapshotBeforeUpdate() 中。

render

這個方法在掛載階段已經講過了,這裏再也不贅述。

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState) {
  //...
}
複製代碼

getSnapshotBeforeUpdate 生命週期方法在 render 以後,在更新以前(如:更新 DOM 以前)被調用。給了一個機會去獲取 DOM 信息,計算獲得並返回一個 snapshot,這個 snapshot 會做爲 componentDidUpdate 的第三個參數傳入。若是你不想要返回值,請返回 null,不寫的話控制檯會有警告。

而且,這個方法必定要和 componentDidUpdate 一塊兒使用,不然控制檯也會有警告。getSnapshotBeforeUpdatecomponentDidUpdate 一塊兒,這個新的生命週期涵蓋過期的 UNSAFE_componentWillUpdate 的全部用例。

getSnapshotBeforeUpdate(prevProps, prevState) {
  console.log('#enter getSnapshotBeforeUpdate');
  return 'foo';
}

componentDidUpdate(prevProps, prevState, snapshot) {
  console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
複製代碼

上面這段代碼能夠看出來這個 snapshot 怎麼個用法,snapshot 乍一看還覺得是組件級別的某個「快照」,其實能夠是任何值,到底怎麼用徹底看開發者本身,getSnapshotBeforeUpdatesnapshot 返回,而後 DOM 改變,而後 snapshot 傳遞給 componentDidUpdate

官方給了一個例子,用 getSnapshotBeforeUpdate 來處理 scroll,而且說明了一般不須要這個函數,只有在從新渲染過程當中手動保留滾動位置等狀況下很是有用,因此大部分開發者都用不上,也就不要亂用。

componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot) {
  //...
}
複製代碼

componentDidUpdate() 會在更新後會被當即調用。首次渲染不會執行此方法。在這個函數裏咱們能夠操做 DOM,和發起服務器請求,還能夠 setState,可是注意必定要用 if 語句控制,不然會致使無限循環。

componentDidUpdate(prevProps) {
  // 典型用法(不要忘記比較 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}
複製代碼

若是組件實現了 getSnapshotBeforeUpdate() 生命週期,則它的返回值將做爲 componentDidUpdate() 的第三個參數 snapshot 參數傳遞。不然此參數將爲 undefined。

卸載階段

卸載階段,這個階段的生命週期函數只有一個:

componentWillUnmount

componentWillUnmount() 會在組件卸載及銷燬以前直接調用。咱們能夠在此方法中執行必要的清理操做,例如,清除 timer,取消網絡請求或清除在 componentDidMount() 中建立的訂閱等。注意不要在這個函數裏調用 setState(),由於組件不會從新渲染了。

其餘不經常使用的生命週期函數

還有兩個很不經常使用的生命週期函數,在這也列一下。

詳細使用示例請見:React 官方文檔

static getDerivedStateFromError()

static getDerivedStateFromError(error) {
  //...
}
複製代碼

今生命週期會在後代組件拋出錯誤後被調用。它將拋出的錯誤做爲參數,並返回一個值以更新 stategetDerivedStateFromError() 會在渲染階段調用,所以不容許出現反作用。如遇此類狀況,請改用 componentDidCatch()

componentDidCatch()

componentDidCatch(error, info) {
  //...
}
複製代碼

今生命週期在後代組件拋出錯誤後被調用。它接收兩個參數:

  1. error —— 拋出的錯誤。
  2. info —— 帶有 componentStack key 的對象,其中包含有關組件引起錯誤的棧信息。

componentDidCatch() 會在「提交」階段被調用,所以容許執行反作用。它應該用於記錄錯誤之類的狀況:

若是發生錯誤,你能夠經過調用 setState 使用 componentDidCatch() 渲染降級 UI,但在將來的版本中將不推薦這樣作。可使用靜態 getDerivedStateFromError() 來處理降級渲染。

參考資料

本文參考瞭如下文章和官方文檔,推薦閱讀。

結語

有人會說,如今都 Hooks 一把梭了,你總結整合這些內容有啥用。其實學習這些內容,可以幫助你加深對 React 的理解,深刻領會 React 的思想。而且,目前 Class component 與 Hooks 是並存的,雖然新項目通常都直接用 Hooks,可是老項目中不免會遇到 Class component,因此仍是要學會的。


更多精彩內容,微信掃碼關注公衆號「技術漫談」:

  • LeetCode 算法題解
  • JavaScript 入門到進階
  • 前端項目從零到一實戰
  • ……

相關文章
相關標籤/搜索