重新認識 React 生命週期

發表於2018-09-19

前言

React 從 v16 開始,像是跨入了新的時代,效能和新的 API 都令人矚目。重新認識 React,從重新認識生命週期開始。

為了更好的支援非同步渲染(Async Rendering),解決一些生命週期濫用可能導致的問題,React 從 V16.3 開始,對生命週期進行漸進式調整,同時在官方文件也提供了使用的最佳實踐。

這裡我們將簡要對比 React 新舊生命週期,重新認識一下 React 生命週期。

 

新的生命週期

先看看兩張經典的生命週期的示意圖
舊的生命週期

舊的生命週期

新的生命週期

新的生命週期

React 16.3 新增的生命週期方法

  1. getDerivedStateFromProps()
  2. getSnapshotBeforeUpdate()

逐漸廢棄的生命週期方法:

  1. componentWillMount()
  2. componentWillReceiveProps()
  3. componentWillUpdate()

雖然廢棄了這三個生命週期方法,但是為了向下相容,將會做漸進式調整。(詳情見#12028
V16.3 並未刪除這三個生命週期,同時還為它們新增以 UNSAFE_ 字首為別名的三個函式 UNSAFE_componentWillMount()UNSAFE_componentWillReceiveProps()UNSAFE_componentWillUpdate()
在 16.4 版本給出警告將會棄用 componentWillMount()componentWillReceiveProps()componentWillUpdate() 三個函式
然後在 17 版本將會刪除 componentWillMount()componentWillReceiveProps()componentWillUpdate() 這三個函式,會保留使用 UNSAFE_componentWillMount()UNSAFE_componentWillReceiveProps()UNSAFE_componentWillUpdate()

一般將生命週期分成三個階段:

  1. 建立階段(Mounting)
  2. 更新階段(Updating)
  3. 解除安裝階段(Unmounting)

從 React v16 開始,還對生命週期加入了錯誤處理(Error Handling)。

下面分析一下生命週期的各個階段。

建立階段 Mounting

元件例項建立並插入 DOM 時,按順序呼叫以下方法:

  • constructor()
  • static getDerivedStateFromProps()
  • componentWillMount()/UNSAFE_componentWillMount()(being deprecated)
  • render()
  • componentDidMount()

有定義 getDerivedStateFromProps 時,會忽略 componentWillMount()/UNSAFE_componentWillMount()
詳情檢視原始碼

constructor()

建構函式通常用於:

  • 使用 this.state 來初始化 state
  • 給事件處理函式繫結 this

注意:ES6 子類的建構函式必須執行一次 super()。React 如果建構函式中要使用 this.props,必須先執行 super(props)。

static getDerivedStateFromProps()

當建立時、接收新的 props 時、setState 時、forceUpdate 時會執行這個方法。

注意:v16.3 setState 時、forceUpdate 時不會執行這個方法,v16.4 修復了這個問題。

這是一個 靜態方法,引數 nextProps 是新接收的 propsprevState 是當前的 state。返回值(物件)將用於更新 state,如果不需要更新則需要返回 null

下面是官方文件給出的例子

這個方法的常用作用也很明顯了:父元件傳入新的 props 時,用來和當前的 state 對比,判斷是否需要更新 state。以前一般使用 componentWillReceiveProps 做這個操作。

這個方法在建議儘量少用,只在必要的場景中使用,一般使用場景如下:

  1. 無條件的根據 props 更新 state
  2. propsstate 的不匹配情況更新 state

詳情可以參考官方文件的最佳實踐 You Probably Don’t Need Derived State

componentWillMount()/UNSAFE_componentWillMount()(棄用)

這個方法已經不推薦使用。因為在未來非同步渲染機制下,該方法可能會多次呼叫。它所行使的功能也可以由 componentDidMount()constructor() 代替:

  • 之前有些人會把非同步請求放在這個生命週期,其實大部分情況下都推薦把非同步資料請求放在 componentDidMount() 中。
  • 在服務端渲染時,通常使用 componentWillMount() 獲取必要的同步資料,但是可以使用 constructor() 代替它。

可以使用 setState,不會觸發 re-render

render

每個類元件中,render() 唯一必須的方法。

render() 正如其名,作為渲染用,可以返回下面幾種型別:

  • React 元素(React elements)
  • 陣列(Arrays)
  • 片段(fragments)
  • 插槽(Portals)
  • 字串或數字(String and numbers)
  • 布林值或 null(Booleans or null)

注意:
Arrays 和 String 是 v16.0.0 新增。
fragments 是 v16.2.0 新增。
Portals 是 V16.0.0 新增。

裡面不應該包含副作用,應該作為純函式。

不能使用 setState。

componentDidMount()

元件完成裝載(已經插入 DOM 樹)時,觸發該方法。這個階段已經獲取到真實的 DOM。

一般用於下面的場景:

  • 非同步請求 ajax
  • 新增事件繫結(注意在 componentWillUnmount 中取消,以免造成記憶體洩漏)

可以使用 setState,觸發re-render,影響效能。

更新階段 Updating

  • componentWillReceiveProps()/UNSAFE_componentWillReceiveProps()(being deprecated)
  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • componentWillUpdate()/UNSAFE_componentWillUpdate()(being deprecated)
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

getDerivedStateFromProps 或者 getSnapshotBeforeUpdate 時,componentWillReceiveProps()/UNSAFE_componentWillReceiveProps()componentWillUpdate()/UNSAFE_componentWillUpdate() 不會執行 (詳情檢視原始碼

componentWillReceiveProps()/UNSAFE_componentWillReceiveProps()(棄用)

這個方法在接收新的 props 時觸發,即使 props 沒有變化也會觸發。

一般用這個方法來判斷 props 的前後變化來更新 state,如下面的例子:

這個方法將被棄用,推薦使用 getDerivedStateFromProps 代替。

可以使用 setState

static getDerivedStateFromProps()

同 Mounting 時所述一致。

shouldComponentUpdate()

在接收新的 props 或新的 state 時,在渲染前會觸發該方法。

該方法通過返回 true 或者 false 來確定是否需要觸發新的渲染。返回 false, 則不會觸發後續的 UNSAFE_componentWillUpdate()render()componentDidUpdate()(但是 state 變化還是可能引起子元件重新渲染)。

所以通常通過這個方法對 propsstate 做比較,從而避免一些不必要的渲染。

PureComponent 的原理就是對 propsstate 進行淺對比(shallow comparison),來判斷是否觸發渲染。

componentWillUpdate()/UNSAFE_componentWillUpdate() (棄用)

當接收到新的 props 或 state 時,在渲染前執行該方法。

在以後非同步渲染時,可能會出現某些元件暫緩更新,導致 componentWillUpdatecomponentDidUpdate 之間的時間變長,這個過程中可能發生一些變化,比如使用者行為導致 DOM 發生了新的變化,這時在 componentWillUpdate 獲取的資訊可能就不可靠了。

不能使用 setState

render()

同 Mounting 時所述一致。

getSnapshotBeforeUpdate()

這個方法在 render() 之後,componentDidUpdate() 之前呼叫。

兩個引數 prevProps 表示更新前的 propsprevState 表示更新前的 state

返回值稱為一個快照(snapshot),如果不需要 snapshot,則必須顯示的返回 null —— 因為返回值將作為 componentDidUpdate() 的第三個引數使用。所以這個函式必須要配合 componentDidUpdate() 一起使用。

這個函式的作用是在真實 DOM 更新(componentDidUpdate)前,獲取一些需要的資訊(類似快照功能),然後作為引數傳給 componentDidUpdate。例如:在 getSnapShotBeforeUpdate 中獲取滾動位置,然後作為引數傳給 componentDidUpdate,就可以直接在渲染真實的 DOM 時就滾動到需要的位置。

下面是官方文件給出的例子:

componentDidUpdate()

這個方法是在更新完成之後呼叫,第三個引數 snapshot 就是 getSnapshotBeforeUpdate 的返回值。

正如前面所說,有 getSnapshotBeforeUpdate 時,必須要有 componentDidUpdate。所以這個方法的一個應用場景就是上面看到的例子,配合 getSnapshotBeforeUpdate 使用。

可以使用 setState,會觸發 re-render,所以要注意判斷,避免導致死迴圈。

解除安裝階段 Unmounting

  • componentWillUnmount()

componentWillUnmount

在元件解除安裝或者銷燬前呼叫。這個方法主要用來做一些清理工作,例如:

  • 取消定時器
  • 取消事件繫結
  • 取消網路請求

不能使用 setState

錯誤處理 Error Handling

  • componentDidCatch()

componentDidCatch()

任何子元件在渲染期間,生命週期方法中或者建構函式 constructor 發生錯誤時呼叫。

錯誤邊界不會捕獲下面的錯誤:

  • 事件處理 (Event handlers) (因為事件處理不發生在 React 渲染時,報錯不影響渲染)
  • 非同步程式碼 (Asynchronous code) (e.g. setTimeout or requestAnimationFrame callbacks)
  • 服務端渲染 (Server side rendering)
  • 錯誤邊界本身(而不是子元件)丟擲的錯誤

總結

React 生命週期可以檢視 生命週期圖

雖然 React 有做向下相容,但是推薦儘量避免使用廢棄的生命週期,而是擁抱未來,用新的生命週期替換它們。

如果你不想升級 React,但是想用新的生命週期方法,也是可以的。使用 react-lifecycles-compat polyfill,可以為低版本的 React(0.14.9+)提供新的生命週期方法。

參考:
React.Component
Update on Async Rendering

相關文章