一個月前,React 官方正式發佈了 v16.3 版本。在此次的更新中,除了前段時間被熱烈討論的新 Context API 以外,新引入的兩個生命週期函數 getDerivedStateFromProps
,getSnapshotBeforeUpdate
以及在將來 v17.0 版本中即將被移除的三個生命週期函數 componentWillMount
,componentWillReceiveProps
,componentWillUpdate
也很是值得咱們花點時間去探究一下其背後的緣由以及在具體項目中的升級方案。javascript
在 React 應用中,許多開發者爲了不第一次渲染時頁面由於沒有獲取到異步數據致使的白屏,而將數據請求部分的代碼放在了 componentWillMount
中,但願能夠避免白屏並提前異步請求的發送時間。但事實上在 componentWillMount
執行後,第一次渲染就已經開始了,因此若是在 componentWillMount
執行時尚未獲取到異步數據的話,頁面首次渲染時也仍然會處於沒有異步數據的狀態。換句話說,組件在首次渲染時老是會處於沒有異步數據的狀態,因此不論在哪裏發送數據請求,都沒法直接解決這一問題。而關於提前發送數據請求,官方也鼓勵將數據請求部分的代碼放在組件的 constructor
中,而不是 componentWillMount
。java
另外一個常見的 componentWillMount
的用例是在服務端渲染時獲取數據,由於在服務端渲染時 componentDidMount
是不會被調用的。針對這個問題,筆者這裏提供兩種解法。第一個簡單的解法是將全部的數據請求都放在 componentDidMount
中,即只在客戶端請求異步數據。這樣作能夠避免在服務端和客戶端分別請求兩次相同的數據(componentWillMount
在客戶端渲染時一樣會被調用到),但很明顯的缺點就是沒法在服務端渲染時獲取到頁面渲染所需的全部數據,因此若是咱們須要保證服務端返回的 HTML 就是用戶最終看到的 HTML 的話,咱們能夠將每一個頁面的數據獲取邏輯單獨抽離出來,而後一一對應到相應的頁面,在服務端根據當前頁面的路由找到相應的數據請求,利用鏈式的 Promise 在渲染最終的頁面前就將數據塞入 redux store 或其餘數據管理工具中,這樣服務端返回的 HTML 就是包含異步數據的結果了。git
另外一個常見的用例是在 componentWillMount
中訂閱事件,並在 componentWillUnmount
中取消掉相應的事件訂閱。但事實上 React 並不可以保證在 componentWillMount
被調用後,同一組件的 componentWillUnmount
也必定會被調用。一個當前版本的例子如服務端渲染時,componentWillUnmount
是不會在服務端被調用的,因此在 componentWillMount
中訂閱事件就會直接致使服務端的內存泄漏。另外一方面,在將來 React 開啓異步渲染模式後,在 componentWillMount
被調用以後,組件的渲染也頗有可能會被其餘的事務所打斷,致使 componentWillUnmount
不會被調用。而 componentDidMount
就不存在這個問題,在 componentDidMount
被調用後,componentWillUnmount
必定會隨後被調用到,並根據具體代碼清除掉組件中存在的事件訂閱。github
將現有 componentWillMount
中的代碼遷移至 componentDidMount
便可。redux
在老版本的 React 中,若是組件自身的某個 state 跟其 props 密切相關的話,一直都沒有一種很優雅的處理方式去更新 state,而是須要在 componentWillReceiveProps
中判斷先後兩個 props 是否相同,若是不一樣再將新的 props 更新到相應的 state 上去。這樣作一來會破壞 state 數據的單一數據源,致使組件狀態變得不可預測,另外一方面也會增長組件的重繪次數。相似的業務需求也有不少,如一個能夠橫向滑動的列表,當前高亮的 Tab 顯然隸屬於列表自身的狀態,但不少狀況下,業務需求會要求從外部跳轉至列表時,根據傳入的某個值,直接定位到某個 Tab。安全
在新版本中,React 官方提供了一個更爲簡潔的生命週期函數:markdown
static getDerivedStateFromProps(nextProps, prevState) 複製代碼
一個簡單的例子以下:框架
// before componentWillReceiveProps(nextProps) { if (nextProps.translateX !== this.props.translateX) { this.setState({ translateX: nextProps.translateX, }); } } // after static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.translateX !== prevState.translateX) { return { translateX: nextProps.translateX, }; } return null; } 複製代碼
乍看下來這兩者好像並無什麼本質上的區別,但這倒是筆者認爲很是可以體現 React 團隊對於軟件工程深入理解的一個改動,即 React 團隊試圖經過框架級別的 API 來約束或者說幫助開發者寫出可維護性更佳的 JavaScript 代碼。爲了解釋這點,咱們再來看一段代碼:異步
// 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) { return { isLogin: nextProps.isLogin, }; } return null; } componentDidUpdate(prevProps, prevState) { if (!prevState.isLogin && this.props.isLogin) { this.handleClose(); } } 複製代碼
一般來說,在 componentWillReceiveProps
中,咱們通常會作如下兩件事,一是根據 props 來更新 state,二是觸發一些回調,如動畫或頁面跳轉等。在老版本的 React 中,這兩件事咱們都須要在 componentWillReceiveProps
中去作。而在新版本中,官方將更新 state 與觸發回調從新分配到了 getDerivedStateFromProps
與 componentDidUpdate
中,使得組件總體的更新邏輯更爲清晰。並且在 getDerivedStateFromProps
中還禁止了組件去訪問 this.props,強制讓開發者去比較 nextProps 與 prevState 中的值,以確保當開發者用到 getDerivedStateFromProps
這個生命週期函數時,就是在根據當前的 props 來更新組件的 state,而不是去作其餘一些讓組件自身狀態變得更加不可預測的事情。函數
將現有 componentWillReceiveProps
中的代碼根據更新 state 或回調,分別在 getDerivedStateFromProps
及 componentDidUpdate
中進行相應的重寫便可,注意新老生命週期函數中 prevProps
,this.props
,nextProps
,prevState
,this.state
的不一樣。
與 componentWillReceiveProps
相似,許多開發者也會在 componentWillUpdate
中根據 props 的變化去觸發一些回調。但不管是 componentWillReceiveProps
仍是 componentWillUpdate
,都有可能在一次更新中被調用屢次,也就是說寫在這裏的回調函數也有可能會被調用屢次,這顯然是不可取的。與 componentDidMount
相似,componentDidUpdate
也不存在這樣的問題,一次更新中 componentDidUpdate
只會被調用一次,因此將原先寫在 componentWillUpdate
中的回調遷移至 componentDidUpdate
就能夠解決這個問題。
另外一個常見的 componentWillUpdate
的用例是在組件更新前,讀取當前某個 DOM 元素的狀態,並在 componentDidUpdate
中進行相應的處理。但在 React 開啓異步渲染模式後,render 階段和 commit 階段之間並非無縫銜接的,也就是說在 render 階段讀取到的 DOM 元素狀態並不老是和 commit 階段相同,這就致使在 componentDidUpdate
中使用 componentWillUpdate
中讀取到的 DOM 元素狀態是不安全的,由於這時的值頗有可能已經失效了。
爲了解決上面提到的這個問題,React 提供了一個新的生命週期函數:
getSnapshotBeforeUpdate(prevProps, prevState)
複製代碼
與 componentWillUpdate
不一樣,getSnapshotBeforeUpdate
會在最終的 render 以前被調用,也就是說在 getSnapshotBeforeUpdate
中讀取到的 DOM 元素狀態是能夠保證與 componentDidUpdate
中一致的。雖然 getSnapshotBeforeUpdate
不是一個靜態方法,但咱們也應該儘可能使用它去返回一個值。這個值會隨後被傳入到 componentDidUpdate
中,而後咱們就能夠在 componentDidUpdate
中去更新組件的狀態,而不是在 getSnapshotBeforeUpdate
中直接更新組件狀態。
官方提供的一個例子以下:
class ScrollingList extends React.Component { listRef = null; 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) { return ( this.listRef.scrollHeight - this.listRef.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) { this.listRef.scrollTop = this.listRef.scrollHeight - snapshot; } } render() { return ( <div ref={this.setListRef}> {/* ...contents... */} </div> ); } setListRef = ref => { this.listRef = ref; }; } 複製代碼
將現有的 componentWillUpdate
中的回調函數遷移至 componentDidUpdate
。若是觸發某些回調函數時須要用到 DOM 元素的狀態,則將對比或計算的過程遷移至 getSnapshotBeforeUpdate
,而後在 componentDidUpdate
中統一觸發回調或更新狀態。
最後,讓咱們從總體的角度再來看一下 React 此次生命週期函數調整先後的異同:
在第一張圖中被紅框圈起來的三個生命週期函數就是在新版本中即將被移除的。經過上述的兩張圖,咱們能夠清楚地看到將要被移除的三個生命週期函數都是在 render 以前會被調用到的。而根據原來的設計,在這三個生命週期函數中均可以去作一些諸如發送請求,setState 等包含反作用的事情。在老版本的 React 中,這樣作也許只會帶來一些性能上的損耗,但在 React 開啓異步渲染模式以後,就沒法再接受這樣的反作用產生了。舉一個 Git 的例子就是在開發者 commit 了 10 個文件更新後,又對當前或其餘的文件作了另外的更新,但在 push 時卻仍然只 push 了剛纔 commit 的 10 個文件更新。這樣就會致使提交記錄與實際更新不符,若是想要避免這個問題,就須要保證每一次的文件更新都要通過 commit 階段,再被提交到遠端,而這也就是 React 在開啓異步渲染模式以後要作到的。
另外一方面,爲了驗證我的的理解及測試新版本的穩定性,筆者已經將我的負責的幾個項目所有都升級到了 React 16.3 並根據上述提到的升級方案替換了全部即將被移除的生命週期函數。目前,全部項目在生產環境中都運行良好,沒有收到任何不良的用戶反饋。
固然,以上的這些生命週期函數的改動,一直要到 React 17.0 中才會實裝,這給廣大的 React 開發者們預留了充足的時間去適應此次改動。但若是你是 React 開源項目(尤爲是組件庫)的維護者的話,不妨花點時間去詳細瞭解一下此次生命週期函數的改動。由於這不只僅能夠幫助你將開源項目更好地升級到 React 的最新版本,更重要的是能夠幫助你提早理解即將到來的異步渲染模式。
同時,筆者也相信在 React 正式開啓異步渲染模式以後,許多經常使用組件的性能將頗有可能迎來一次總體的提高。進一步來講,配合異步渲染,許多如今的複雜組件均可以被處理得更加優雅,在代碼層面獲得更精細粒度上的控制,並最終爲用戶帶來更加直觀的使用體驗。
我的 Blog 地址:AlanWei/blog