面試題:Hooks 與 React 生命周期的關(guān)系

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)參考?父子組件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 作為一個(gè)合格的開發(fā)者,不要只滿足于編寫了可以運(yùn)行的代碼。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個(gè)周閱讀 8,679評(píng)論 1 33
  • 組件的生命周期 React中組件也有生命周期,也就是說也有很多鉤子函數(shù)供我們使用, 組件的生命周期,我們會(huì)分為四個(gè)...
    解勾股閱讀 838評(píng)論 0 0
  • 起步 安裝官方腳手架: npm install -g create-react-app 創(chuàng)建項(xiàng)目: create-...
    Twoold閱讀 1,556評(píng)論 0 0
  • 40、React 什么是React?React 是一個(gè)用于構(gòu)建用戶界面的框架(采用的是MVC模式):集中處理VIE...
    萌妹撒閱讀 1,191評(píng)論 0 1
  • 在React Native中使用組件來封裝界面模塊時(shí),整個(gè)界面就是一個(gè)大的組件,開發(fā)過程就是不斷優(yōu)化和拆分界面組件...
    朱_源浩閱讀 9,064評(píng)論 6 38

友情鏈接更多精彩內(nèi)容