前言
React 從 v16 開始,像是跨入了新的時代,效能和新的 API 都令人矚目。重新認識 React,從重新認識生命週期開始。
為了更好的支援非同步渲染(Async Rendering),解決一些生命週期濫用可能導致的問題,React 從 V16.3 開始,對生命週期進行漸進式調整,同時在官方文件也提供了使用的最佳實踐。
這裡我們將簡要對比 React 新舊生命週期,重新認識一下 React 生命週期。
新的生命週期
先看看兩張經典的生命週期的示意圖
舊的生命週期
新的生命週期
React 16.3 新增的生命週期方法
- getDerivedStateFromProps()
- getSnapshotBeforeUpdate()
逐漸廢棄的生命週期方法:
- componentWillMount()
- componentWillReceiveProps()
- 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()
一般將生命週期分成三個階段:
- 建立階段(Mounting)
- 更新階段(Updating)
- 解除安裝階段(Unmounting)
從 React v16 開始,還對生命週期加入了錯誤處理(Error Handling)。
下面分析一下生命週期的各個階段。
建立階段 Mounting
元件例項建立並插入 DOM 時,按順序呼叫以下方法:
- constructor()
- static getDerivedStateFromProps()
componentWillMount()/UNSAFE_componentWillMount()(being deprecated)- render()
- componentDidMount()
有定義
getDerivedStateFromProps
時,會忽略componentWillMount()/UNSAFE_componentWillMount()
(詳情檢視原始碼)
constructor()
1 |
constructor(props) |
建構函式通常用於:
- 使用
this.state
來初始化state
- 給事件處理函式繫結
this
注意:ES6 子類的建構函式必須執行一次 super()。React 如果建構函式中要使用 this.props,必須先執行 super(props)。
static getDerivedStateFromProps()
1 |
static getDerivedStateFromProps(nextProps, prevState) |
當建立時、接收新的 props 時、setState 時、forceUpdate 時會執行這個方法。
注意:v16.3 setState 時、forceUpdate 時不會執行這個方法,v16.4 修復了這個問題。
這是一個 靜態方法,引數 nextProps
是新接收的 props
,prevState
是當前的 state
。返回值(物件)將用於更新 state
,如果不需要更新則需要返回 null
。
下面是官方文件給出的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class ExampleComponent extends React.Component { // Initialize state in constructor, // Or with a property initializer. state = { isScrollingDown: false, lastRow: null, }; static getDerivedStateFromProps(props, state) { if (props.currentRow !== state.lastRow) { return { isScrollingDown: props.currentRow > state.lastRow, lastRow: props.currentRow, }; } // Return null to indicate no change to state. return null; } |
這個方法的常用作用也很明顯了:父元件傳入新的 props
時,用來和當前的 state
對比,判斷是否需要更新 state
。以前一般使用 componentWillReceiveProps
做這個操作。
這個方法在建議儘量少用,只在必要的場景中使用,一般使用場景如下:
- 無條件的根據
props
更新state
- 當
props
和state
的不匹配情況更新state
詳情可以參考官方文件的最佳實踐 You Probably Don’t Need Derived State
componentWillMount()/UNSAFE_componentWillMount()(棄用)
1 |
UNSAFE_componentWillMount() |
這個方法已經不推薦使用。因為在未來非同步渲染機制下,該方法可能會多次呼叫。它所行使的功能也可以由 componentDidMount()
和 constructor()
代替:
- 之前有些人會把非同步請求放在這個生命週期,其實大部分情況下都推薦把非同步資料請求放在
componentDidMount()
中。 - 在服務端渲染時,通常使用
componentWillMount()
獲取必要的同步資料,但是可以使用constructor()
代替它。
可以使用 setState,不會觸發 re-render
render
1 |
rander() |
每個類元件中,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()
1 |
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()(棄用)
1 |
UNSAFE_componentWillReceiveProps(nextProps) |
這個方法在接收新的 props 時觸發,即使 props 沒有變化也會觸發。
一般用這個方法來判斷 props 的前後變化來更新 state
,如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class ExampleComponent extends React.Component { state = { isScrollingDown: false, }; componentWillReceiveProps(nextProps) { if (this.props.currentRow !== nextProps.currentRow) { this.setState({ isScrollingDown: nextProps.currentRow > this.props.currentRow, }); } } } |
這個方法將被棄用,推薦使用 getDerivedStateFromProps
代替。
可以使用 setState
static getDerivedStateFromProps()
同 Mounting 時所述一致。
shouldComponentUpdate()
在接收新的 props
或新的 state
時,在渲染前會觸發該方法。
該方法通過返回 true
或者 false
來確定是否需要觸發新的渲染。返回 false
, 則不會觸發後續的 UNSAFE_componentWillUpdate()
、render()
和 componentDidUpdate()
(但是 state
變化還是可能引起子元件重新渲染)。
所以通常通過這個方法對 props
和 state
做比較,從而避免一些不必要的渲染。
PureComponent 的原理就是對
props
和state
進行淺對比(shallow comparison),來判斷是否觸發渲染。
componentWillUpdate()/UNSAFE_componentWillUpdate() (棄用)
1 |
UNSAFE_componentWillUpdate(nextProps, nextState) |
當接收到新的 props 或 state 時,在渲染前執行該方法。
在以後非同步渲染時,可能會出現某些元件暫緩更新,導致 componentWillUpdate
和 componentDidUpdate
之間的時間變長,這個過程中可能發生一些變化,比如使用者行為導致 DOM 發生了新的變化,這時在 componentWillUpdate
獲取的資訊可能就不可靠了。
不能使用 setState
render()
同 Mounting 時所述一致。
getSnapshotBeforeUpdate()
1 |
getSnapShotBeforeUpdate(prevProps, prevState) |
這個方法在 render()
之後,componentDidUpdate()
之前呼叫。
兩個引數 prevProps
表示更新前的 props
,prevState
表示更新前的 state
。
返回值稱為一個快照(snapshot),如果不需要 snapshot,則必須顯示的返回 null
—— 因為返回值將作為 componentDidUpdate()
的第三個引數使用。所以這個函式必須要配合 componentDidUpdate()
一起使用。
這個函式的作用是在真實 DOM 更新(componentDidUpdate
)前,獲取一些需要的資訊(類似快照功能),然後作為引數傳給 componentDidUpdate
。例如:在 getSnapShotBeforeUpdate
中獲取滾動位置,然後作為引數傳給 componentDidUpdate
,就可以直接在渲染真實的 DOM 時就滾動到需要的位置。
下面是官方文件給出的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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()
1 |
componentDidUpdate(prevProps, prevState, snapshot) |
這個方法是在更新完成之後呼叫,第三個引數 snapshot
就是 getSnapshotBeforeUpdate
的返回值。
正如前面所說,有 getSnapshotBeforeUpdate
時,必須要有 componentDidUpdate
。所以這個方法的一個應用場景就是上面看到的例子,配合 getSnapshotBeforeUpdate
使用。
可以使用 setState,會觸發 re-render,所以要注意判斷,避免導致死迴圈。
解除安裝階段 Unmounting
- componentWillUnmount()
componentWillUnmount
1 |
componentWillUnmount() |
在元件解除安裝或者銷燬前呼叫。這個方法主要用來做一些清理工作,例如:
- 取消定時器
- 取消事件繫結
- 取消網路請求
不能使用 setState
錯誤處理 Error Handling
- componentDidCatch()
componentDidCatch()
1 |
componentDidCatch(err, info) |
任何子元件在渲染期間,生命週期方法中或者建構函式 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+)提供新的生命週期方法。