在寫這篇文章的時候,React 已經出了 17.0.1 版本了,雖然說還來討論目前 React 新舊生命週期有點晚了,React 兩個新生命週期雖然出了好久,但實際開發我卻沒有用過,由於 React 16 版本後咱們直接 React Hook 起飛開發項目。javascript
但對新舊生命週期的探索,仍是有助於咱們更好理解 React 團隊一些思想和作法,因而今天就要回顧下這個問題和理解總結,雖然仍是 React Hook 寫法香,可是依然要深究學習類組件的東西,瞭解 React 團隊的一些思想與作法。html
本文只討論 React17 版本前的。java
首先是給三個生命週期函數加上了 UNSAFE:react
這裏並非表示不安全的意思,它只是不建議繼續使用,並表示使用這些生命週期的代碼可能在將來的 React 版本(目前 React17 尚未徹底廢除)存在缺陷,如 React Fiber 異步渲染的出現。git
同時新增了兩個生命週期函數:github
UNSAFE_componentWillReceiveProps(nextProps)
先來講說這個函數,componentWillReceiveProps
編程
該子組件方法並非父組件 props 改變才觸發,官方回答是:安全
若是父組件致使組件從新渲染,即便 props 沒有更改,也會調用此方法。若是隻想處理更改,請確保進行當前值與變動值的比較。
先來講說 React 爲何廢除該函數,廢除確定有它很差的地方。服務器
componentWillReceiveProps
函數的通常使用場景是:異步
但該方法缺點是會破壞 state 數據的單一數據源,致使組件狀態變得不可預測,另外一方面也會增長組件的重繪次數。
而在新版本中,官方將更新 state 與觸發回調從新分配到了 getDerivedStateFromProps
與 componentDidUpdate
中,使得組件總體的更新邏輯更爲清晰。
新生命週期方法static getDerivedStateFromProps(props, state)
怎麼用呢?
getDerivedStateFromProps 會在調用 render 方法以前調用,而且在初始掛載及後續更新時都會被調用。它應返回一個對象來更新 state,若是返回 null 則不更新任何內容。
從函數名字就能夠看出大概意思:使用 props 來派生/更新 state。這就是重點了,但凡你想使用該函數,都必須出於該目的,使用它纔是正確且符合規範的。
跟getDerivedStateFromProps
不一樣的是,它在掛載和更新階段都會執行(componentWillReceiveProps
掛載階段不會執行),由於更新 state 這種需求不只在 props 更新時存在,在 props 初始化時也是存在的。
並且getDerivedStateFromProps
在組件自身 state 更新也會執行而componentWillReceiveProps
方法執行則取決於父組件的是否觸發從新渲染,也能夠看出getDerivedStateFromProps
並非 componentWillReceiveProps
方法的替代品.
引發咱們注意的是,這個生命週期方法是一個靜態方法,靜態方法不依賴組件實例而存在,故在該方法內部是沒法訪問 this 的。新版本生命週期方法能作的事情反而更少了,限制咱們只能根據 props 來派生 state,官方是基於什麼考量呢?
由於沒法拿到組件實例的 this,這也致使咱們沒法在函數內部作 this.fetch()請求,或者不合理的 this.setState()操做致使可能的死循環或其餘反作用。有沒有發現,這都是不合理不規範的操做,但開發者們都有機會這樣用。可若是加了個靜態 static,間接強制咱們都沒法作了,也從而避免對生命週期的濫用。
React 官方也是經過該限制,儘可能保持生命週期行爲的可控可預測,根源上幫助了咱們避免不合理的編程方式,即一個 API 要保持單一性,作一件事的理念。
以下例子:
// before componentWillReceiveProps(nextProps) { if (nextProps.isLogin !== this.props.isLogin) { this.setState({ isLogin: nextProps.isLogin, }); } if (nextProps.isLogin) { this.handleClose(); } } // after static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.isLogin !== prevState.isLogin) { // 被對比的props會被保存一份在state裏 return { isLogin: nextProps.isLogin, // getDerivedStateFromProps 的返回值會自動 setState }; } return null; } componentDidUpdate(prevProps, prevState) { if (!prevState.isLogin && this.props.isLogin) { this.handleClose(); } }
UNSAFE_componentWillMount() 在掛載以前被調用。它在 render() 以前調用,所以在此方法中同步調用 setState() 不會觸發額外渲染。
咱們應該避免在此方法中引入任何反作用或事件訂閱,而是選用componentDidMount()
。
在 React 初學者剛接觸的時候,可能有這樣一個疑問:通常都是數據請求放在componentDidMount
裏面,但放在componentWillMount
不是會更快獲取數據嗎?
由於理解是componentWillMount
在 render 以前執行,早一點執行就早拿到請求結果;可是其實無論你請求多快,都趕不上首次 render,頁面首次渲染依舊處於沒有獲取異步數據的狀態。
還有一個緣由,componentWillMount
是服務端渲染惟一會調用的生命週期函數,若是你在此方法中請求數據,那麼服務端渲染的時候,在服務端和客戶端都會分別請求兩次相同的數據,這顯然也咱們想看到的結果。
特別是有了 React Fiber,更有機會被調用屢次,故請求不該該放在componentWillMount
中。
還有一個錯誤的使用是在componentWillMount
中訂閱事件,並在componentWillUnmount
中取消掉相應的事件訂閱。事實上只有調用componentDidMount
後,React 才能保證稍後調用componentWillUnmount
進行清理。並且服務端渲染時不會調用componentWillUnmount
,可能致使內存泄露。
還有人會將事件監聽器(或訂閱)添加到 componentWillMount
中,但這可能致使服務器渲染(永遠不會調用 componentWillUnmount
)和異步渲染(在渲染完成以前可能被中斷,致使不調用 componentWillUnmount
)的內存泄漏。
對於該函數,通常狀況,若是項目有使用,則是一般把現有 componentWillMount
中的代碼遷移至 componentDidMount
便可。
當組件收到新的 props 或 state 時,會在渲染以前調用
UNSAFE_componentWillUpdate()
。使用此做爲在更新發生以前執行準備更新的機會。初始渲染不會調用此方法。
注意,不能在該方法中調用 this.setState();在 componentWillUpdate
返回以前,你也不該該執行任何其餘操做(例如,dispatch Redux 的 action)觸發對 React 組件的更新。
首先跟上面兩個函數同樣,該函數也發生在 render 以前,也存在一次更新被調用屢次的可能,從這一點上看就依然不可取了。
其次,該方法常見的用法是在組件更新前,讀取當前某個 DOM 元素的狀態,並在 componentDidUpdate
中進行相應的處理。但 React 16 版本後有 suspense、異步渲染機制等等,render 過程能夠被分割成屢次完成,還能夠被暫停甚至回溯,這致使 componentWillUpdate
和 componentDidUpdate
執行先後可能會間隔很長時間,這致使 DOM 元素狀態是不安全的,由於這時的值頗有可能已經失效了。並且足夠使用戶進行交互操做更改當前組件的狀態,這樣可能會致使難以追蹤的 BUG。
爲了解決這個問題,因而就有了新的生命週期函數:
getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate
在最近一次渲染輸出(提交到 DOM 節點)以前調用。它使得組件能在發生更改以前從 DOM 中捕獲一些信息(例如,滾動位置)。今生命週期的任何返回值將做爲第三個參數傳入componentDidUpdate(prevProps, prevState, snapshot)
與 componentWillUpdate
不一樣,getSnapshotBeforeUpdate
會在最終的 render 以前被調用,也就是說在 getSnapshotBeforeUpdate
中讀取到的 DOM 元素狀態是能夠保證與 componentDidUpdate
中一致的。
雖然 getSnapshotBeforeUpdate
不是一個靜態方法,但咱們也應該儘可能使用它去返回一個值。這個值會隨後被傳入到 componentDidUpdate
中,而後咱們就能夠在 componentDidUpdate
中去更新組件的狀態,而不是在 getSnapshotBeforeUpdate
中直接更新組件狀態。避免了 componentWillUpdate
和 componentDidUpdate
配合使用時將組件臨時的狀態數據存在組件實例上浪費內存,getSnapshotBeforeUpdate
返回的數據在 componentDidUpdate
中用完即被銷燬,效率更高。
來看官方的一個例子:
class ScrollingList extends React.Component { constructor(props) { super(props); this.listRef = React.createRef(); } getSnapshotBeforeUpdate(prevProps, prevState) { // 咱們是否在 list 中添加新的 items? // 捕獲滾動位置以便咱們稍後調整滾動位置。 if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; return list.scrollHeight - list.scrollTop; } return null; } componentDidUpdate(prevProps, prevState, snapshot) { // 若是咱們 snapshot 有值,說明咱們剛剛添加了新的 items, // 調整滾動位置使得這些新 items 不會將舊的 items 推出視圖。 //(這裏的 snapshot 是 getSnapshotBeforeUpdate 的返回值) if (snapshot !== null) { const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; } } render() { return ( <div ref={this.listRef}>{/* ...contents... */}</div> ); } }
若是項目中有用到componentWillUpdate
的話,升級方案就是將現有的 componentWillUpdate
中的回調函數遷移至 componentDidUpdate
。若是觸發某些回調函數時須要用到 DOM 元素的狀態,則將對比或計算的過程遷移至 getSnapshotBeforeUpdate
,而後在 componentDidUpdate
中統一觸發回調或更新狀態。
除了這些,React 16 版本的依然還有大改動,其中引人注目的就是 Fiber,以後我還會抽空寫一篇關於 React Fiber 的文章,能夠關注個人我的技術博文 Github 倉庫,以爲不錯的話歡迎 star,給我一點鼓勵繼續寫做吧~
參考: