探索 React 組件之間的生命週期

本文首發於個人博客react

0)寫在前面

React 組件的生命週期,相信你們都很是熟悉了,無非那麼幾個函數,官方文檔已經寫得很是清楚了。(那還有什麼好說的?浪費感情!合上!)git

通常咱們所討論的,都是單個組件的生命週期。若是是多個組件之間呢?好比父子組件?兄弟組件?各個週期又是什麼樣的?異步路由的狀況呢?前陣子新出的 Hooks 呢?有幾我的敢站出來講我全知道的?(反正我是不敢)github

恰好也是最近遇到一些關於生命週期的問題,項目中涉及到大量的異步操做,須要清楚地知道各部分的執行順序,藉此機會整理一下。數組

1)在你繼續以前

這篇文章並非入門教學,若是你對 React 一點不瞭解的話,或許這篇文章並不適合你。瀏覽器

我假定你已經掌握 React 的基本知識,例如:組件的生命週期、Hooks 的基本概念、類組件和函數組件的區別 等,並用 React 開發過有必定複雜度的應用。併發

這裏咱們不討論 shouldComponentUpdate()React.memo() 等優化手段,只考慮最原始的狀況。異步

本文以瀏覽器做爲目標環境,React Native 和 Electron 在基本概念上是同樣的,細節上的不一樣不做爲本文的討論重點,函數

2)關於 Hooks 的生命週期

確切地說,Hooks 並非一種新的組件類型,它只是一種代碼複用的方式,而且老是伴隨着函數組件一塊兒出現。優化

在 Hooks 以前,函數組件是沒有 state 的概念的,於是也就不存在生命週期一說,就只是一個 render 函數。Hooks 的出現,讓函數組件也能夠擁有 state,相應的也就引入了生命週期的概念,具體來講也就是 useEffect()useLayoutEffect() 具體什麼時候執行的問題。設計

函數組件的本質是函數,而函數自己是沒有生命週期的,Hooks 的出現也沒有改變這一點。這裏咱們討論的對象是「組件」,組件是能夠有生命週期的。所以當我在後面的文字中提到 Hooks 時,我實際上是在表示「使用了 Hooks 的函數組件」(雖然這個說法不是很嚴謹,可是這不重要,你懂我意思就好)。

3)那麼咱們就來作個實驗吧

爲了一探究竟,我寫了一個 Demo 來模擬一些常見的用例:父子組件、兄弟組件、同步/異步路由、類組件和 Hooks、組件初始化時的異步操做(如訪問 API)等。

若是你有遇到 Demo 沒覆蓋到的使用場景,歡迎提 Issue。

3.1)TL,DR;

我知道你們的時間都很寶貴,趕時間的朋友能夠直接看結論;時間寬裕的朋友,咱們從下一節開始細聊:

  1. 同步路由,父組件在 render 階段建立子組件。
  2. 異步路由,父組件在自身掛載完成以後纔開始建立子組件。
  3. 掛載完成以後,在更新時,同步組件和異步組件是同樣的。
  4. 不管是掛載仍是更新,以 render 完成爲界,以前父組件先執行,以後子組件先執行。
  5. 兄弟組件大致上按照在父組件中的出場順序執行。
  6. useEffect 會在掛載/更新完成以後,延遲執行。
  7. 異步請求(如訪問 API)什麼時候獲得響應與組件的生命週期無關,即父組件中發起的異步請求不保證在子組件掛載完成前獲得響應。

3.2)掛載過程

父子組件的掛載分爲三個階段。

第一階段,父組件執行到自身的 render,解析其下有哪些子組件須要渲染,並對其中同步的子組件進行建立,挨個執行各組件到 render,生成到目前爲止的 Virtual DOM 樹,並 commit 到 DOM。

第二階段,此時 DOM 節點已經生成完畢,組件掛載完成,開始後續流程。先依次觸發同步子組件各自的 componentDidMount / useLayoutEffect,最後觸發父組件的。

第三階段,若是組件使用了 useEffect,則會在第二階段以後觸發 useEffect。若是父子組件都使用了 useEffect,那麼子組件先觸發,而後是父組件。

若是父組件中包含異步子組件,則會在父組件掛載完成後被建立。

對於兄弟組件,若是是同步路由,它們的建立順序和在父組件中定義的出場順序是一致的。

對於「異步的兄弟組件」,最終的加載順序是按照 JSX 中定義的順序,仍是按照 js 文件下載完成的順序,我暫時還不能肯定。

按照我對「異步」的理解,我更傾向於認爲是按照下載完成的順序,這更符合「按需加載」的概念。

之因此會形成困擾,是由於據我目前所觀察到的狀況,兩種順序是一致的,我尚未遇到事後定義但先加載的狀況。

大部分時候咱們會以頁面爲單位去劃分異步組件,單個頁面須要加載多個異步組件的場景比較少;即使在這些少數場景中,單次須要請求的文件數量也不會不少,不至於超過瀏覽器的併發上限;即使超過,也會按照在父組件中定義的出場順序去分批發起請求。考慮到單個異步組件的文件尺寸一般都很小,加載速度很是快,同一批發起的請求基本上也都是同時到達,所以大部分時候下載完成的順序和定義的順序是一致的。

但沒遇到不表明不存在,該問題我會進一步驗證,已經有結果的小夥伴也能夠分享一下。

若是組件的初始化過程包含異步操做(一般在 componentDidMount()useEffect(fn, []) 中進行),這些操做什麼時候獲得響應與組件的生命週期無關,徹底看異步操做自己花了多少時間。

3.3)更新過程

React 的設計遵循單向數據流模型,兄弟節點之間的通訊也會通過父組件(Redux 和 Context 也是經過改變父組件傳遞下來的 props 實現的),所以任何兩個組件之間的通訊,本質上均可以歸結爲父組件更新致使子組件更新的狀況。

父子組件的更新一樣分爲三個階段。

第1、三階段,和掛載過程基本同樣,無非是第一階段多了一個 Reconciliation 的過程,第三階段須要先執行 useEffect 的 Cleanup 函數。

第二階段,和掛載過程也很相似,都是子組件先於父組件,但更新比掛載涉及的函數要多一些:

  1. getSnapshotBeforeUpdate()
  2. useLayoutEffect() 的 Cleanup
  3. useLayoutEffect() / componentDidUpdate()

React 會按照上面的順序依次執行這些函數,每一個函數都是各個子組件的先執行,而後纔是父組件的執行。具體說來,就是先執行各個子組件的 getSnapshotBeforeUpdate(),而後是父組件的 getSnapshotBeforeUpdate(),再而後是各個子組件的 componentDidUpdate(),父組件的 componentDidUpdate(),以此類推。

這裏咱們把類組件和 Hooks 的生命週期函數放在了一塊兒,由於父子組件能夠是這兩種組件類型的任意排列組合。實際渲染時不必定每個函數都有用到,只會調用組件實際擁有的函數。

3.4)卸載過程

卸載過程涉及到 componentWillUnmount()useEffect() 的 Cleanup、useLayoutEffect() 的 Cleanup 這三種函數,順序固定爲父組件的先執行,子組件按照在 JSX 中定義的順序依次執行各自的方法。

注意,此時的 Cleanup 函數會按照在代碼中定義的順序前後執行,與函數自己的特性無關。

若是卸載舊組件的同時伴隨有新組件的建立,新組件會先被建立並執行完 render,而後卸載不須要的舊組件,最後新組件執行掛載完成的回調。

4)Hooks 的特別之處

根據 React 的官方文檔,useEffect()useLayoutEffect() 都是等效於 componentDidUpdate() / componentDidMount() 的存在,但實際上二者在一些細節上仍是有所不一樣:

4.1)先來未必先走

useLayoutEffect() 永遠比 useEffect() 先執行,即使在你的代碼中 useEffect() 是寫在前面的。因此 useLayoutEffect() 纔是事實上和 componentDidUpdate() / componentDidMount() 分庭抗禮的存在。

useEffect() 會在父子組件的 componentDidUpdate() / componentDidMount() 都觸發以後才被觸發。當父子組件都用到 useEffect() 時,子組件中的會比父組件中的先觸發。

4.2)不團結的 Cleanup

一樣都擁有 Cleanup 函數,useLayoutEffect() 和它的 Cleanup 未必是挨着的。

當父組件是 Hooks、子組件是 Class 時,可以很明顯看出,useLayoutEffect() 的 Cleanup 會在 getSnapshotBeforeUpdate()componentDidUpdate() 之間被調用,而 useLayoutEffect() 則是和 componentDidUpdate() 同級,按照更新過程的順序被調用。

Hooks 做爲子組件時也是這麼個過程,只是沒有了子組件,看上去不那麼明顯罷了。

useEffect() 就不同,它和它的 Cleanup 緊密團結在一塊兒,每次執行都是先後腳一塊兒的,從不分離。

5)小結

不管是類組件仍是 Hooks,單拎出來你們確定都很熟悉它們的生命週期,但當把它們混在一塊兒,就沒那麼簡單了。撰寫這篇博客的過程,幫助我理清了這通亂麻,希望也可以幫到堅持看到這裏的你。

做者聯繫方式
相關文章
相關標籤/搜索