網上有不少關於 React 生命週期的文章,我也看了很多,爲了梳理並加深我對此的理解,因此決定寫這篇文章。本文主要梳理目前最新的 V16.4 的生命週期函數。如今 React 最新版本是 16.13,可是生命週期最新版本是 16.4,以後版本的生命週期沒有過改動了,本文不涉及 Hooks。html
先上示意圖:前端
React 在 V16.3 版本中,爲下面三個生命週期函數加上了 UNSAFE
:react
UNSAFE_componentWillMount
UNSAFE_componentWillReceiveProps
UNSAFE_componentWillUpdate
標題中的廢棄不是指真的廢棄,只是不建議繼續使用,並表示在 V17.0 版本中正式刪除。先來講說 React 爲何要這麼作。算法
主要是這些生命週期方法常常被誤用和濫用。而且在 React V16.0 以前,React 是同步渲染的,而在 V16.0 以後 React 更新了其渲染機制,是經過異步的方式進行渲染的,在 render
函數以前的全部函數都有可能被執行屢次。數組
長期以來,原有的生命週期函數老是會誘惑開發者在 render
以前的生命週期函數中作一些動做,如今這些動做還放在這些函數中的話,有可能會被調用屢次,這確定不是咱們想要的結果。瀏覽器
有一個常見的問題,有人問爲何不在 UNSAFE_componentWillMount
中寫 AJAX 獲取數據的功能,他們的觀點是,UNSAFE_componentWillMount
在 render
以前執行,早一點執行早獲得結果。可是要知道,在 UNSAFE_componentWillMount
中發起 AJAX 請求,無論多快獲得結果也趕不上首次 render,數據都是要在 render
後才能到達。安全
並且 UNSAFE_componentWillMount
在服務器端渲染也會被調用到(此方法是服務端渲染惟一會調用的生命週期函數。),你確定不但願 AJAX 請求被執行屢次,因此這樣的 IO 操做放在 componentDidMount
中更合適。性能優化
尤爲是在 Fiber 啓用了異步渲染以後,更沒有理由在 UNSAFE_componentWillMount
中進行 AJAX 請求了,由於 UNSAFE_componentWillMount
可能會被調用屢次,誰也不會但願無謂地屢次調用 AJAX 吧。服務器
還有人會將事件監聽器(或訂閱)添加到 UNSAFE_componentWillMount
中,但這可能致使服務器渲染(永遠不會調用 componentWillUnmount
)和異步渲染(在渲染完成以前可能被中斷,致使不調用 componentWillUnmount
)的內存泄漏。微信
人們一般認爲 UNSAFE_componentWillMount
和 componentWillUnmount
是成對出現的,但這並不能保證。只有調用了 componentDidMount
以後,React 才能保證稍後調用 componentWillUnmount
進行清理。所以,添加監聽器/訂閱的推薦方法是使用 componentDidMount
生命週期。
有時候組件在 props
發生變化時會產生反作用。與 UNSAFE_componentWillUpdate
相似,UNSAFE_componentWillReceiveProps
可能在一次更新中被屢次調用。所以,避免在此方法中產生反作用很是重要。相反,應該使用 componentDidUpdate
,由於它保證每次更新只調用一次。
UNSAFE_componentWillReceiveProps
是考慮到由於父組件引起渲染可能要根據 props
更新 state
的須要而設立的。新的 getDerivedStateFromProps
實際上與 componentDidUpdate
一塊兒取代了之前的 UNSAFE_componentWillReceiveProps
函數。
有些人使用 UNSAFE_componentWillUpdate
是出於一種錯誤的擔憂,即當 componentDidUpdate
觸發時,更新其餘組件的 state
已經」太晚」了。事實並不是如此。React 可確保在用戶看到更新的 UI 以前,刷新在 componentDidMount
和 componentDidUpdate
期間發生的任何 setState
調用。
一般,最好避免這樣的級聯更新。固然在某些狀況下,這些更新也是必需的(例如:若是你須要在測量渲染的 DOM 元素後,定位工具的提示)。無論怎樣,在異步模式下使用 UNSAFE_componentWillUpdate
都是不安全的,由於外部回調可能會在一次更新中被屢次調用。相反,應該使用 componentDidUpdate
生命週期,由於它保證每次更新只調用一次。
大多數開發者使用 UNSAFE_componentWillUpdate
的場景是配合 componentDidUpdate
,分別獲取 rerender
先後的視圖狀態,進行必要的處理。但隨着 React 新的 suspense
、time slicing
、異步渲染等機制的到來,render
過程能夠被分割成屢次完成,還能夠被暫停甚至回溯,這致使 UNSAFE_componentWillUpdate
和 componentDidUpdate
執行先後可能會間隔很長時間,足夠使用戶進行交互操做更改當前組件的狀態,這樣可能會致使難以追蹤的 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
,而在調用了 setState
和 forceUpdate
時則不會。
React 官方也發現了這個問題,並在 React V16.4 版本中進行了修復。也就是說在更新階段中,接收到新的 props
,調用了 setState
和 forceUpdate
時都會調用 static getDerivedStateFromProps
。具體在下面講到這個函數的時候有詳細說明。
React 生命週期主要分爲三個階段:
掛載階段也能夠理解爲初始化階段,也就是把咱們的組件插入到 DOM 中。這個階段的過程以下:
constructor
getDerivedStateFromProps
UNSAVE_componentWillMount
render
componentDidMount
組件的構造函數,第一個被執行。若是在組件中沒有顯示定義它,則會擁有一個默認的構造函數。若是咱們顯示定義構造函數,則必須在構造函數第一行執行 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);
}
複製代碼
使用方式:
//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
,調用了 setState
和 forceUpdate
時被調用。
新的 getDerivedStateFromProps
實際上與 componentDidUpdate
一塊兒取代了之前的 UNSAFE_componentWillReceiveProps
函數。UNSAFE_componentWillReceiveProps
也是考慮到由於父組件引起渲染可能要根據 props
更新 state
的須要而設立的。
UNSAVE_componentWillMount
UNSAFE_componentWillMount()
在掛載以前被調用。它在 render()
以前調用,所以在此方法中同步調用 setState()
不會觸發額外渲染。一般,咱們建議使用 constructor()
來初始化 state。
避免在此方法中引入任何反作用或訂閱。如遇此種狀況,請改用 componentDidMount()
。
此方法是服務端渲染惟一會調用的生命週期函數。UNSAFE_componentWillMount()
經常使用於當支持服務器渲染時,須要同步獲取數據的場景。
這是 React 中最核心的方法,class 組件中惟一必須實現的方法。
當 render
被調用時,它會檢查 this.props
和 this.state
的變化並返回如下類型之一:
render()
函數應該是一個純函數,裏面只作一件事,就是返回須要渲染的東西,不該該包含其它的業務邏輯,如數據請求,對於這些業務邏輯請移到 componentDidMount
和 componentDidUpdate
中。
componentDidMount()
會在組件掛載後(插入 DOM 樹中)當即調用。依賴於 DOM 節點的初始化應該放在這裏。如需經過網絡請求獲取數據,此處是實例化請求的好地方。這個方法是比較適合添加訂閱的地方。若是添加了訂閱,請不要忘記在 componentWillUnmount()
裏取消訂閱
你能夠在 componentDidMount()
裏直接調用 setState()
。它將觸發額外渲染,但此渲染會發生在瀏覽器更新屏幕以前。如此保證了即便在 render()
兩次調用的狀況下,用戶也不會看到中間狀態。
請謹慎使用該模式,由於它會致使性能問題。一般,你應該在 constructor()
中初始化 state
。若是你的渲染依賴於 DOM 節點的大小或位置,好比實現 modals
和 tooltips
等狀況下,你可使用此方式處理
更新階段是指當組件的 props 發生了改變,或組件內部調用了 setState 或者發生了 forceUpdate,則進行更新。
這個階段的過程以下:
UNSAFE_componentWillReceiveProps
getDerivedStateFromProps
shouldComponentUpdate
UNSAFE_componentWillUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
UNSAFE_componentWillReceiveProps
UNSAFE_componentWillReceiveProps
是考慮到由於父組件引起渲染可能要根據 props
更新 state
的須要而設立的。UNSAFE_componentWillReceiveProps
會在已掛載的組件接收新的 props
以前被調用。若是你須要更新狀態以響應 prop
更改(例如,重置它),你能夠比較 this.props
和 nextProps
並在此方法中使用 this.setState()
執行 state
轉換。
若是父組件致使組件從新渲染,即便 props
沒有更改,也會調用此方法。若是隻想處理更改,請確保進行當前值與變動值的比較。在掛載過程當中,React 不會針對初始 props
調用 UNSAFE_componentWillReceiveProps()
。組件只會在組件的 props
更新時調用此方法。調用 this.setState()
一般不會觸發 UNSAFE_componentWillReceiveProps()
。
這個方法在掛載階段已經講過了,這裏再也不贅述。記住該函數會在掛載時,在更新時接收到新的 props
,調用了 setState
和 forceUpdate
時被調用。它與 componentDidUpdate
一塊兒取代了之前的 UNSAFE_componentWillReceiveProps
函數。
shouldComponentUpdate(nextProps, nextState) {
//...
}
複製代碼
它有兩個參數,根據此函數的返回值來判斷是否進行從新渲染,true
表示從新渲染,false
表示不從新渲染,默認返回 true
。注意,首次渲染或者當咱們調用 forceUpdate
時並不會觸發此方法。此方法僅用於性能優化。
由於默認是返回 true
,也就是隻要接收到新的屬性和調用了 setState
都會觸發從新的渲染,這會帶來必定的性能問題,因此咱們須要將 this.props
與 nextProps
以及 this.state
與 nextState
進行比較來決定是否返回 false
,來減小從新渲染,以優化性能。請注意,返回 false
並不會阻止子組件在 state
更改時從新渲染。
可是官方提倡咱們使用內置的 PureComponent
來減小從新渲染的次數,而不是手動編寫 shouldComponentUpdate
代碼。PureComponent
內部實現了對 props 和 state
進行淺層比較。
若是 shouldComponentUpdate()
返回 false
,則不會調用 UNSAFE_componentWillUpdate()
,render()
和 componentDidUpdate()
。官方說在後續版本,React 可能會將 shouldComponentUpdate
視爲提示而不是嚴格的指令,而且,當返回 false
時,仍可能致使組件從新渲染。
UNSAFE_componentWillUpdate
當組件收到新的 props
或 state
時,會在渲染以前調用 UNSAFE_componentWillUpdate()
。使用此做爲在更新發生以前執行準備更新的機會。初始渲染不會調用此方法。可是你不能此方法中調用 this.setState()
。在 UNSAFE_componentWillUpdate()
返回以前,你也不該該執行任何其餘操做(例如,dispatch
Redux 的 action
)觸發對 React 組件的更新。
一般,此方法能夠替換爲 componentDidUpdate()
。若是你在此方法中讀取 DOM 信息(例如,爲了保存滾動位置),則能夠將此邏輯移至 getSnapshotBeforeUpdate()
中。
這個方法在掛載階段已經講過了,這裏再也不贅述。
getSnapshotBeforeUpdate(prevProps, prevState) {
//...
}
複製代碼
getSnapshotBeforeUpdate
生命週期方法在 render
以後,在更新以前(如:更新 DOM 以前)被調用。給了一個機會去獲取 DOM 信息,計算獲得並返回一個 snapshot
,這個 snapshot
會做爲 componentDidUpdate
的第三個參數傳入。若是你不想要返回值,請返回 null
,不寫的話控制檯會有警告。
而且,這個方法必定要和 componentDidUpdate
一塊兒使用,不然控制檯也會有警告。getSnapshotBeforeUpdate
與 componentDidUpdate
一塊兒,這個新的生命週期涵蓋過期的 UNSAFE_componentWillUpdate
的全部用例。
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('#enter getSnapshotBeforeUpdate');
return 'foo';
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
複製代碼
上面這段代碼能夠看出來這個 snapshot
怎麼個用法,snapshot
乍一看還覺得是組件級別的某個「快照」,其實能夠是任何值,到底怎麼用徹底看開發者本身,getSnapshotBeforeUpdate
把 snapshot
返回,而後 DOM 改變,而後 snapshot
傳遞給 componentDidUpdate
。
官方給了一個例子,用 getSnapshotBeforeUpdate
來處理 scroll
,而且說明了一般不須要這個函數,只有在從新渲染過程當中手動保留滾動位置等狀況下很是有用,因此大部分開發者都用不上,也就不要亂用。
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()
會在組件卸載及銷燬以前直接調用。咱們能夠在此方法中執行必要的清理操做,例如,清除 timer,取消網絡請求或清除在 componentDidMount()
中建立的訂閱等。注意不要在這個函數裏調用 setState()
,由於組件不會從新渲染了。
還有兩個很不經常使用的生命週期函數,在這也列一下。
詳細使用示例請見:React 官方文檔
static getDerivedStateFromError(error) {
//...
}
複製代碼
今生命週期會在後代組件拋出錯誤後被調用。它將拋出的錯誤做爲參數,並返回一個值以更新 state
。getDerivedStateFromError()
會在渲染階段調用,所以不容許出現反作用。如遇此類狀況,請改用 componentDidCatch()
。
componentDidCatch(error, info) {
//...
}
複製代碼
今生命週期在後代組件拋出錯誤後被調用。它接收兩個參數:
componentDidCatch()
會在「提交」階段被調用,所以容許執行反作用。它應該用於記錄錯誤之類的狀況:
若是發生錯誤,你能夠經過調用 setState
使用 componentDidCatch()
渲染降級 UI,但在將來的版本中將不推薦這樣作。可使用靜態 getDerivedStateFromError()
來處理降級渲染。
本文參考瞭如下文章和官方文檔,推薦閱讀。
有人會說,如今都 Hooks 一把梭了,你總結整合這些內容有啥用。其實學習這些內容,可以幫助你加深對 React 的理解,深刻領會 React 的思想。而且,目前 Class component 與 Hooks 是並存的,雖然新項目通常都直接用 Hooks,可是老項目中不免會遇到 Class component,因此仍是要學會的。
更多精彩內容,微信掃碼關注公衆號「技術漫談」: