React 生命周期很多人都了解,但通常我們所了解的都是?單個(gè)組件?的生命周期,但針對(duì)?Hooks 組件、多個(gè)關(guān)聯(lián)組件(父子組件和兄弟組件) 的生命周期又是怎么樣的喃?你有思考和了解過嗎,接下來我們將完整的了解 React 生命周期。
關(guān)于?組件?,我們這里指的是?React.Component?以及?React.PureComponent?,但是否包括 Hooks 組件喃?
一、Hooks 組件
函數(shù)組件?的本質(zhì)是函數(shù),沒有 state 的概念的,因此不存在生命周期一說,僅僅是一個(gè)?render 函數(shù)而已。
但是引入?Hooks?之后就變得不同了,它能讓組件在不使用 class 的情況下?lián)碛?state,所以就有了生命周期的概念,所謂的生命周期其實(shí)就是?useState、?useEffect()?和useLayoutEffect()?。
即:Hooks 組件(使用了Hooks的函數(shù)組件)有生命周期,而函數(shù)組件(未使用Hooks的函數(shù)組件)是沒有生命周期的。
下面,是具體的 class 與 Hooks 的生命周期對(duì)應(yīng)關(guān)系:
constructor:函數(shù)組件不需要構(gòu)造函數(shù),我們可以通過調(diào)用?useState?來初始化 state。如果計(jì)算的代價(jià)比較昂貴,也可以傳一個(gè)函數(shù)給?useState。
const[num,?UpdateNum]?=?useState(0)
getDerivedStateFromProps:一般情況下,我們不需要使用它,我們可以在渲染過程中更新 state,以達(dá)到實(shí)現(xiàn)?getDerivedStateFromProps?的目的。
functionScrollView({row}){
let[isScrollingDown,?setIsScrollingDown]?=?useState(false);
let[prevRow,?setPrevRow]?=?useState(null);
if(row?!==?prevRow)?{
// Row 自上次渲染以來發(fā)生過改變。更新 isScrollingDown。
setIsScrollingDown(prevRow?!==null&&?row?>?prevRow);
setPrevRow(row);
}
return`Scrolling?down:?${isScrollingDown}`;
}
React 會(huì)立即退出第一次渲染并用更新后的 state 重新運(yùn)行組件以避免耗費(fèi)太多性能。
shouldComponentUpdate:可以用?React.memo?包裹一個(gè)組件來對(duì)它的?props?進(jìn)行淺比較
constButton?=?React.memo((props)?=>{
//?具體的組件
});
注意:React.memo?等效于?PureComponent,它只淺比較 props。這里也可以使用useMemo?優(yōu)化每一個(gè)節(jié)點(diǎn)。
render:這是函數(shù)組件體本身。
componentDidMount,?componentDidUpdate:useLayoutEffect?與它們兩的調(diào)用階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當(dāng)它出問題的時(shí)候再嘗試使用useLayoutEffect。useEffect?可以表達(dá)所有這些的組合。
//?componentDidMount
useEffect(()=>{
//?需要在?componentDidMount?執(zhí)行的內(nèi)容
},?[])
useEffect(()=>{
//?在?componentDidMount,以及?count?更改時(shí)?componentDidUpdate?執(zhí)行的內(nèi)容
document.title?=?`You?clicked?${count}?times`;
return()=>{
//?需要在?count?更改時(shí)?componentDidUpdate(先于?document.title?=?...?執(zhí)行,遵守先清理后更新)
//?以及?componentWillUnmount?執(zhí)行的內(nèi)容???????
}//?當(dāng)函數(shù)中?Cleanup?函數(shù)會(huì)按照在代碼中定義的順序先后執(zhí)行,與函數(shù)本身的特性無關(guān)
},?[count]);//?僅在?count?更改時(shí)更新
請(qǐng)記得 React 會(huì)等待瀏覽器完成畫面渲染之后才會(huì)延遲調(diào)用?useEffect,因此會(huì)使得額外操作很方便
componentWillUnmount:相當(dāng)于?useEffect?里面返回的?cleanup?函數(shù)
//?componentDidMount/componentWillUnmount
useEffect(()=>{
//?需要在?componentDidMount?執(zhí)行的內(nèi)容
returnfunctioncleanup(){
//?需要在?componentWillUnmount?執(zhí)行的內(nèi)容??????
}
},?[])
componentDidCatch?and?getDerivedStateFromError:目前還沒有這些方法的 Hook 等價(jià)寫法,但很快會(huì)加上。
為方便記憶,大致匯總成表格如下。
class 組件Hooks 組件
constructoruseState
getDerivedStateFromPropsuseState 里面 update 函數(shù)
shouldComponentUpdateuseMemo
render函數(shù)本身
componentDidMountuseEffect
componentDidUpdateuseEffect
componentWillUnmountuseEffect ?里面返回的函數(shù)
componentDidCatch無
getDerivedStateFromError無
二、單個(gè)組件的生命周期
1. 生命周期
V16.3 之前
我們可以將生命周期分為三個(gè)階段:
掛載階段
組件更新階段
卸載階段
分開來講:
掛載階段
constructor:避免將 props 的值復(fù)制給 state
componentWillMount
render:react 最重要的步驟,創(chuàng)建虛擬 dom,進(jìn)行 diff 算法,更新 dom 樹都在此進(jìn)行
componentDidMount
組件更新階段
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
卸載階段
componentWillUnMount
這種生命周期會(huì)存在一個(gè)問題,那就是當(dāng)更新復(fù)雜組件的最上層組件時(shí),調(diào)用棧會(huì)很長,如果在進(jìn)行復(fù)雜的操作時(shí),就可能長時(shí)間阻塞主線程,帶來不好的用戶體驗(yàn),Fiber?就是為了解決該問題而生。
V16.3 之后
Fiber 本質(zhì)上是一個(gè)虛擬的堆棧幀,新的調(diào)度器會(huì)按照優(yōu)先級(jí)自由調(diào)度這些幀,從而將之前的同步渲染改成了異步渲染,在不影響體驗(yàn)的情況下去分段計(jì)算更新。
對(duì)于異步渲染,分為兩階段:
reconciliation:
componentWillMount
componentWillReceiveProps
shouldConmponentUpdate
componentWillUpdate
commit
componentDidMount
componentDidUpdate
其中,reconciliation?階段是可以被打斷的,所以?reconcilation?階段執(zhí)行的函數(shù)就會(huì)出現(xiàn)多次調(diào)用的情況,顯然,這是不合理的。
所以 V16.3 引入了新的 API 來解決這個(gè)問題:
static getDerivedStateFromProps:該函數(shù)在掛載階段和組件更新階段都會(huì)執(zhí)行,即每次獲取新的props?或?state?之后都會(huì)被執(zhí)行,在掛載階段用來代替componentWillMount;在組件更新階段配合?componentDidUpdate,可以覆蓋componentWillReceiveProps?的所有用法。
同時(shí)它是一個(gè)靜態(tài)函數(shù),所以函數(shù)體內(nèi)不能訪問?this,會(huì)根據(jù)?nextProps?和prevState?計(jì)算出預(yù)期的狀態(tài)改變,返回結(jié)果會(huì)被送給?setState,返回?null?則說明不需要更新?state,并且這個(gè)返回是必須的。
getSnapshotBeforeUpdate: 該函數(shù)會(huì)在?render?之后, DOM 更新前被調(diào)用,用于讀取最新的 DOM 數(shù)據(jù)。
返回一個(gè)值,作為?componentDidUpdate?的第三個(gè)參數(shù);配合?componentDidUpdate, 可以覆蓋componentWillUpdate?的所有用法。
注意:V16.3 中只用在組件掛載或組件?props?更新過程才會(huì)調(diào)用,即如果是因?yàn)樽陨?setState 引發(fā)或者forceUpdate 引發(fā),而不是由父組件引發(fā)的話,那么static getDerivedStateFromProps也不會(huì)被調(diào)用,在 V16.4 中更正為都調(diào)用。
即更新后的生命周期為:
掛載階段
constructor
static getDerivedStateFromProps
render
componentDidMount
更新階段
static getDerivedStateFromProps
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
卸載階段
componentWillUnmount
2. 生命周期,誤區(qū)
誤解一:getDerivedStateFromProps?和?componentWillReceiveProps?只會(huì)在?props?改變?時(shí)才會(huì)調(diào)用
實(shí)際上,只要父級(jí)重新渲染,getDerivedStateFromProps?和?componentWillReceiveProps都會(huì)重新調(diào)用,不管?props?有沒有變化。所以,在這兩個(gè)方法內(nèi)直接將 props 賦值到 state 是不安全的。
//?子組件
classPhoneInputextendsComponent{
state?=?{phone:this.props.phone?};
handleChange?=e=>{
this.setState({phone:?e.target.value?});
};
render()?{
const{?phone?}?=this.state;
return;
}
componentWillReceiveProps(nextProps)?{
//?不要這樣做。
//?這會(huì)覆蓋掉之前所有的組件內(nèi) state 更新!
this.setState({?phone:?nextProps.phone?});
}
}
//?父組件
class?App?extends?Component?{
constructor()?{
super();
this.state?=?{
count:?0
};
}
componentDidMount()?{
//?使用了?setInterval,
//?每秒鐘都會(huì)更新一下?state.count
//?這將導(dǎo)致?App?每秒鐘重新渲染一次
this.interval?=?setInterval(
()?=>
this.setState(prevState?=>?({
count:?prevState.count?+?1
})),
1000
);
}
componentWillUnmount()?{
clearInterval(this.interval);
}
render()?{
return?(
<>
Start?editing?to?see?some?magic?happen?:)
This?component?will?re-render?every?second.?Each?time?it?renders,?the
text?you?type?will?be?reset.?This?illustrates?a?derived?state
anti-pattern.
);
}
}
實(shí)例可點(diǎn)擊這里查看
當(dāng)然,我們可以在 父組件App 中?shouldComponentUpdate?比較 props 的 email 是不是修改再?zèng)Q定要不要重新渲染,但是如果子組件接受多個(gè) props(較為復(fù)雜),就很難處理,而且shouldComponentUpdate?主要是用來性能提升的,不推薦開發(fā)者操作shouldComponetUpdate(可以使用?React.PureComponet)。
我們也可以使用?在 props 變化后修改 state。
classPhoneInputextendsComponent{
state?=?{
phone:this.props.phone
};
componentWillReceiveProps(nextProps)?{
//?只要?props.phone?改變,就改變?state
if(nextProps.phone?!==this.props.phone)?{
this.setState({
phone:?nextProps.phone
});
}
}
//?...
}
但這種也會(huì)導(dǎo)致一個(gè)問題,當(dāng) props 較為復(fù)雜時(shí),props 與 state 的關(guān)系不好控制,可能導(dǎo)致問題
解決方案一:完全可控的組件
functionPhoneInput(props){
return;
}
完全由 props 控制,不派生 state
解決方案二:有 key 的非可控組件
classPhoneInputextendsComponent{
state?=?{phone:this.props.defaultPhone?};
handleChange?=event=>{
this.setState({phone:?event.target.value?});
};
render()?{
return;
}
}
defaultPhone={this.props.user.phone}
key={this.props.user.id}
/>
當(dāng)?key?變化時(shí), React 會(huì)創(chuàng)建一個(gè)新的而不是更新一個(gè)既有的組件
誤解二:將 props 的值直接復(fù)制給 state
應(yīng)避免將 props 的值復(fù)制給 state
constructor(props)?{
super(props);
//?千萬不要這樣做
//?直接用?props,保證單一數(shù)據(jù)源
this.state?=?{phone:?props.phone?};
}
三、多個(gè)組件的執(zhí)行順序
1. 父子組件
掛載階段
分?兩個(gè)?階段:
第?一?階段,由父組件開始執(zhí)行到自身的?render,解析其下有哪些子組件需要渲染,并對(duì)其中?同步的子組件?進(jìn)行創(chuàng)建,按?遞歸順序?挨個(gè)執(zhí)行各個(gè)子組件至?render,生成到父子組件對(duì)應(yīng)的 Virtual DOM 樹,并 commit 到 DOM。
第?二?階段,此時(shí) DOM 節(jié)點(diǎn)已經(jīng)生成完畢,組件掛載完成,開始后續(xù)流程。先依次觸發(fā)同步子組件各自的?componentDidMount,最后觸發(fā)父組件的。
注意:如果父組件中包含異步子組件,則會(huì)在父組件掛載完成后被創(chuàng)建。
所以執(zhí)行順序是:
父組件 getDerivedStateFromProps —> 同步子組件 getDerivedStateFromProps —> 同步子組件 componentDidMount —> 父組件 componentDidMount —> 異步子組件 getDerivedStateFromProps —> 異步子組件 componentDidMount
更新階段
React 的設(shè)計(jì)遵循單向數(shù)據(jù)流模型?,也就是說,數(shù)據(jù)均是由父組件流向子組件。
第?一?階段,由父組件開始,執(zhí)行
更新到自身的?render,解析其下有哪些子組件需要渲染,并對(duì)?子組件?進(jìn)行創(chuàng)建,按?遞歸順序?挨個(gè)執(zhí)行各個(gè)子組件至?render,生成到父子組件對(duì)應(yīng)的 Virtual DOM 樹,并與已有的 Virtual DOM 樹 比較,計(jì)算出?Virtual DOM 真正變化的部分?,并只針對(duì)該部分進(jìn)行的原生DOM操作。
static getDerivedStateFromProps
shouldComponentUpdate
第?二?階段,此時(shí) DOM 節(jié)點(diǎn)已經(jīng)生成完畢,組件掛載完成,開始后續(xù)流程。先依次觸發(fā)同步子組件以下函數(shù),最后觸發(fā)父組件的。
React 會(huì)按照上面的順序依次執(zhí)行這些函數(shù),每個(gè)函數(shù)都是各個(gè)子組件的先執(zhí)行,然后才是父組件的執(zhí)行。
所以執(zhí)行順序是:
父組件 getDerivedStateFromProps —> 父組件 shouldComponentUpdate —> 子組件 getDerivedStateFromProps —> 子組件 shouldComponentUpdate —> 子組件 getSnapshotBeforeUpdate —> ?父組件 getSnapshotBeforeUpdate —> 子組件 componentDidUpdate —> 父組件 componentDidUpdate
getSnapshotBeforeUpdate()
componentDidUpdate()
卸載階段
componentWillUnmount(),順序?yàn)?父組件的先執(zhí)行,子組件按照在 JSX 中定義的順序依次執(zhí)行各自的方法。
注意?:如果卸載舊組件的同時(shí)伴隨有新組件的創(chuàng)建,新組件會(huì)先被創(chuàng)建并執(zhí)行完render,然后卸載不需要的舊組件,最后新組件執(zhí)行掛載完成的回調(diào)。
2. 兄弟組件
掛載階段
若是同步路由,它們的創(chuàng)建順序和其在共同父組件中定義的先后順序是?一致?的。
若是異步路由,它們的創(chuàng)建順序和 js 加載完成的順序一致。
更新階段、卸載階段
兄弟節(jié)點(diǎn)之間的通信主要是經(jīng)過父組件(Redux 和 Context 也是通過改變父組件傳遞下來的?props?實(shí)現(xiàn)的),滿足React 的設(shè)計(jì)遵循單向數(shù)據(jù)流模型,?因此任何兩個(gè)組件之間的通信,本質(zhì)上都可以歸結(jié)為父子組件更新的情況?。
所以,兄弟組件更新、卸載階段,請(qǐng)參考?父子組件。