本篇文章帶大家看看React新版生命周期帶來了哪些變化。
React16.4版本之后使用了新的生命周期,它使用了一些新的生命周期鉤子(getDerivedStateFromProps、getSnapshotBeforeUpdate),并且即將廢棄老版的3個生命周期鉤子(componentWillMount、componentWillReceiveProps、componentWillUpdate)。
一、新版生命周期
如圖所示,我們可以看到,在組件第一次掛載時會經歷:
構造器(constructor)=》修改state屬性(getDerivedStateFromProps)=》組件掛載渲染(render)=》組件掛載完成(componentDidMount)
組件內部狀態更新:
更新state屬性(getDerivedStateFromProps)=》判斷組件是否更新(shouldComponentUpdate)=》組件更新渲染(render)=》(getSnapshotBeforeUpdate)=》組件更新完成(componentDidUpdate)
組件卸載時執行:
組件銷毀(componentWillUnmount)
注意:
新版本使用了getDerivedStateFromProps代替了componentWillMount、componentWillReceiveProps、componentWillUpdate三個鉤子函數,如果在舊版本中使用將會警告提示。它必須要return一個null或者對象,并且會影響初始化的值以及修改的值。
getSnapshotBeforeUpdate鉤子必須與componentDidUpdate搭配使用否則會報錯。在舊版本中使用將會警告提示。必須要return一個null或者任何值,它將在最近一次渲染輸出(提交到DOM節點)之前調用。
二、生命周期新增函數詳解
static getDerivedStateFromProps(getDSFP)
首先這個新的方法是一個靜態方法,在這里不能調用this,也就是一個純函數。它傳了兩個參數,一個是新的nextProps ,一個是之前的prevState,所以只能通過prevState而不是prevProps來做對比,它保證了state和props之間的簡單關系以及不需要處理第一次渲染時prevProps為空的情況。也基于以上兩點,將原本componentWillReceiveProps里進行的更新工作分成兩步來處理,一步是setState狀態變化,更新 state在getDerivedStateFromProps里直接處理。
舊的React中componentWillReceiveProps方法是用來判斷前后兩個props是否相同,如果不同,則將新的props更新到相應的state上去。在這個過程中我們實際上是可以訪問到當前props的,這樣我們可能會對this.props做一些奇奇怪怪的操作,很可能會破壞state數據的單一數據源,導致組件狀態變得不可預測。
而在getDerivedStateFromProps中禁止了組件去訪問 this.props,強制讓開發者去比較nextProps與prevState中的值,以確保當開發者用到getDerivedStateFromProps這個生命周期函數時,就是在根據當前的props來更新組件的state,而不是去訪問this.props并做其他一些讓組件自身狀態變得更加不可預測的事情。
getSnapshotBeforeUpdate
在React開啟異步渲染模式后,在執行函數時讀到的DOM元素狀態并不一定和渲染時相同,這就導致在componentDidUpdate中使用的DOM元素狀態是不安全的(不一定是最新的),因為這時的值很有可能已經失效了。
與componentWillMount不同的是,getSnapshotBeforeUpdate會在最終確定的render執行之前執行,也就是能保證其獲取到的元素狀態與componentDidUpdate中獲取到的元素狀態相同。
這個方法并不常用,但它可能出現在UI處理中,如需要以特殊方式處理滾動位置的聊天線程等。并且會返回snapshot的值或null。例如:
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>
);
}
}
上述例子中重點是從 getSnapshotBeforeUpdate讀取scrollHeight屬性,因為“render”階段生命周期(如 render)和“commit”階段生命周期(如 getSnapshotBeforeUpdate 和 componentDidUpdate)之間可能存在延遲。
生命周期修改的深層原因
因為React 16引入了Fiber機制,把同步的渲染流程進化為了異步的渲染流程,這么做的原因是同步渲染流程有個弊端:一旦開始就不能停下,大工作量的渲染任務執行時,主線程會被長時間的占用,瀏覽器無法即時響應與用戶的交互。
Fiber機制會把渲染任務拆解為多個小任務,并且每執行完一個小任務,就把主線程的執行權交出去,也就解決了上面的弊端。
然而,采用Fiber機制進行渲染時,render階段沒有副作用,可以被暫停,終止或重新啟動。就是這個重新啟動,會導致工作在render階段的componentWillMount、componentWillReceiveProps、componentWillUpdate存在重復執行的可能,所以它們幾個必須被替換掉。
三、Error boundaries
在React16之前,組件內的JS錯誤會導致React的內部狀態被破壞,并且在下一次渲染時產生無法追蹤的錯誤。這些錯誤基本上是由其他的非React組件代碼錯誤引起的。但React并沒有提供一種優雅的錯誤處理方式,也無法從錯誤中恢復。
然而部分UI的JS錯誤不應該導致整個應用的崩潰,為了解決這個問題,React引入了一個新的概念——錯誤邊界。
Error boundaries(錯誤邊界)是一個React組件,它可以捕獲并打印發生在其子組件數任何位置的JS錯誤,然后,渲染出備用UI,而非崩潰了的子組件。錯誤邊界可以在渲染期間、生命周期方法和整個組件樹的構造函數中捕獲錯誤。
但是錯誤邊界無法捕獲以下幾種錯誤:
- 事件處理
- 異步代碼
- 服務端渲染
- 自身錯誤
新版React給我們提供了兩個與錯誤處理相關的API:static getDerivedStateFromError和componentDidCatch。
只要在Class組件中定義static getDerivedStateFromError或componentDidCatch這兩個生命周期方法中的任意一個/兩個,那么這個組件就是一個錯誤邊界組件了。
static getDerivedStateFromError
getDerivedStateFromError是一個靜態方法。在渲染子組件的過程中,頁面更新之前,當發生了錯誤時,該方法就會運行。
它將拋出的錯誤作為參數并返回一個對象覆蓋掉當前組件的state。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) { // 更新 state 使下一次渲染可以顯降級 UI return { hasError: true }; }
render() {
if (this.state.hasError) { // 你可以渲染任何自定義的降級 UI return <h1>Something went wrong.</h1>; }
return this.props.children;
}
}
componentDidCatch
componentDidCatch會在后代組件拋出錯誤時調用,因為它的執行時間比較晚,所以一般不支持改變組件state。
它接受兩個參數:
error:拋出的錯誤;
info: 帶有componentStack key的對象,其中包含有關組件引發錯誤的棧信息。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以顯示降級 UI
return { hasError: true };
}
componentDidCatch(error, info) { // "組件堆棧" 例子: // in ComponentThatThrows (created by App) // in ErrorBoundary (created by App) // in div (created by App) // in App logComponentStackToMyService(info.componentStack); }
render() {
if (this.state.hasError) {
// 你可以渲染任何自定義的降級 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
四、總結
React 16基于兩個原因做出了生命周期的調整:
- 其一:為同步渲染改異步渲染的Fiber鋪路,把有可能多次執行的render階段中 componentWillMount/componentillUpdate/componentWillRecevieProps三個方法棄用;
- 其二:為在一定程度上防止用戶對生命周期的錯用和濫用,把新增的getDerivedStateFromProps用static修飾,阻止用戶在其內部使用this。