天天看點

React生命周期詳解(新版)

作者:尚矽谷教育

上一期給大家詳細講解了舊版生命周期,本篇文章帶大家看看React新版生命周期帶來了哪些變化。

React16.4版本之後使用了新的生命周期,它使用了一些新的生命周期鈎子(getDerivedStateFromProps、getSnapshotBeforeUpdate),并且即将廢棄老版的3個生命周期鈎子(componentWillMount、componentWillReceiveProps、componentWillUpdate)。

一、新版生命周期

React生命周期詳解(新版)

如圖所示,我們可以看到,在元件第一次挂載時會經曆:

構造器(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。