本文首發於個人博客。react
React 組件的生命週期,相信你們都很是熟悉了,無非那麼幾個函數,官方文檔已經寫得很是清楚了。(那還有什麼好說的?浪費感情!合上!)git
通常咱們所討論的,都是單個組件的生命週期。若是是多個組件之間呢?好比父子組件?兄弟組件?各個週期又是什麼樣的?異步路由的狀況呢?前陣子新出的 Hooks 呢?有幾我的敢站出來講我全知道的?(反正我是不敢)github
恰好也是最近遇到一些關於生命週期的問題,項目中涉及到大量的異步操做,須要清楚地知道各部分的執行順序,藉此機會整理一下。數組
這篇文章並非入門教學,若是你對 React 一點不瞭解的話,或許這篇文章並不適合你。瀏覽器
我假定你已經掌握 React 的基本知識,例如:組件的生命週期、Hooks 的基本概念、類組件和函數組件的區別 等,並用 React 開發過有必定複雜度的應用。併發
這裏咱們不討論 shouldComponentUpdate()
、React.memo()
等優化手段,只考慮最原始的狀況。異步
本文以瀏覽器做爲目標環境,React Native 和 Electron 在基本概念上是同樣的,細節上的不一樣不做爲本文的討論重點,函數
確切地說,Hooks 並非一種新的組件類型,它只是一種代碼複用的方式,而且老是伴隨着函數組件一塊兒出現。優化
在 Hooks 以前,函數組件是沒有 state 的概念的,於是也就不存在生命週期一說,就只是一個 render 函數。Hooks 的出現,讓函數組件也能夠擁有 state,相應的也就引入了生命週期的概念,具體來講也就是 useEffect()
和 useLayoutEffect()
具體什麼時候執行的問題。設計
函數組件的本質是函數,而函數自己是沒有生命週期的,Hooks 的出現也沒有改變這一點。這裏咱們討論的對象是「組件」,組件是能夠有生命週期的。所以當我在後面的文字中提到 Hooks 時,我實際上是在表示「使用了 Hooks 的函數組件」(雖然這個說法不是很嚴謹,可是這不重要,你懂我意思就好)。
爲了一探究竟,我寫了一個 Demo 來模擬一些常見的用例:父子組件、兄弟組件、同步/異步路由、類組件和 Hooks、組件初始化時的異步操做(如訪問 API)等。
若是你有遇到 Demo 沒覆蓋到的使用場景,歡迎提 Issue。
我知道你們的時間都很寶貴,趕時間的朋友能夠直接看結論;時間寬裕的朋友,咱們從下一節開始細聊:
render
階段建立子組件。render
完成爲界,以前父組件先執行,以後子組件先執行。useEffect
會在掛載/更新完成以後,延遲執行。父子組件的掛載分爲三個階段。
第一階段,父組件執行到自身的 render
,解析其下有哪些子組件須要渲染,並對其中同步的子組件進行建立,挨個執行各組件到 render
,生成到目前爲止的 Virtual DOM 樹,並 commit 到 DOM。
第二階段,此時 DOM 節點已經生成完畢,組件掛載完成,開始後續流程。先依次觸發同步子組件各自的 componentDidMount
/ useLayoutEffect
,最後觸發父組件的。
第三階段,若是組件使用了 useEffect
,則會在第二階段以後觸發 useEffect
。若是父子組件都使用了 useEffect
,那麼子組件先觸發,而後是父組件。
若是父組件中包含異步子組件,則會在父組件掛載完成後被建立。
對於兄弟組件,若是是同步路由,它們的建立順序和在父組件中定義的出場順序是一致的。
對於「異步的兄弟組件」,最終的加載順序是按照 JSX 中定義的順序,仍是按照 js 文件下載完成的順序,我暫時還不能肯定。
按照我對「異步」的理解,我更傾向於認爲是按照下載完成的順序,這更符合「按需加載」的概念。
之因此會形成困擾,是由於據我目前所觀察到的狀況,兩種順序是一致的,我尚未遇到事後定義但先加載的狀況。
大部分時候咱們會以頁面爲單位去劃分異步組件,單個頁面須要加載多個異步組件的場景比較少;即使在這些少數場景中,單次須要請求的文件數量也不會不少,不至於超過瀏覽器的併發上限;即使超過,也會按照在父組件中定義的出場順序去分批發起請求。考慮到單個異步組件的文件尺寸一般都很小,加載速度很是快,同一批發起的請求基本上也都是同時到達,所以大部分時候下載完成的順序和定義的順序是一致的。
但沒遇到不表明不存在,該問題我會進一步驗證,已經有結果的小夥伴也能夠分享一下。
若是組件的初始化過程包含異步操做(一般在 componentDidMount()
和 useEffect(fn, [])
中進行),這些操做什麼時候獲得響應與組件的生命週期無關,徹底看異步操做自己花了多少時間。
React 的設計遵循單向數據流模型,兄弟節點之間的通訊也會通過父組件(Redux 和 Context 也是經過改變父組件傳遞下來的 props
實現的),所以任何兩個組件之間的通訊,本質上均可以歸結爲父組件更新致使子組件更新的狀況。
父子組件的更新一樣分爲三個階段。
第1、三階段,和掛載過程基本同樣,無非是第一階段多了一個 Reconciliation 的過程,第三階段須要先執行 useEffect
的 Cleanup 函數。
第二階段,和掛載過程也很相似,都是子組件先於父組件,但更新比掛載涉及的函數要多一些:
getSnapshotBeforeUpdate()
useLayoutEffect() 的 Cleanup
useLayoutEffect()
/ componentDidUpdate()
React 會按照上面的順序依次執行這些函數,每一個函數都是各個子組件的先執行,而後纔是父組件的執行。具體說來,就是先執行各個子組件的 getSnapshotBeforeUpdate()
,而後是父組件的 getSnapshotBeforeUpdate()
,再而後是各個子組件的 componentDidUpdate()
,父組件的 componentDidUpdate()
,以此類推。
這裏咱們把類組件和 Hooks 的生命週期函數放在了一塊兒,由於父子組件能夠是這兩種組件類型的任意排列組合。實際渲染時不必定每個函數都有用到,只會調用組件實際擁有的函數。
卸載過程涉及到 componentWillUnmount()
、useEffect()
的 Cleanup、useLayoutEffect()
的 Cleanup 這三種函數,順序固定爲父組件的先執行,子組件按照在 JSX 中定義的順序依次執行各自的方法。
注意,此時的 Cleanup 函數會按照在代碼中定義的順序前後執行,與函數自己的特性無關。
若是卸載舊組件的同時伴隨有新組件的建立,新組件會先被建立並執行完 render
,而後卸載不須要的舊組件,最後新組件執行掛載完成的回調。
根據 React 的官方文檔,useEffect()
和 useLayoutEffect()
都是等效於 componentDidUpdate()
/ componentDidMount()
的存在,但實際上二者在一些細節上仍是有所不一樣:
useLayoutEffect()
永遠比 useEffect()
先執行,即使在你的代碼中 useEffect()
是寫在前面的。因此 useLayoutEffect()
纔是事實上和 componentDidUpdate()
/ componentDidMount()
分庭抗禮的存在。
useEffect()
會在父子組件的 componentDidUpdate()
/ componentDidMount()
都觸發以後才被觸發。當父子組件都用到 useEffect()
時,子組件中的會比父組件中的先觸發。
一樣都擁有 Cleanup 函數,useLayoutEffect()
和它的 Cleanup 未必是挨着的。
當父組件是 Hooks、子組件是 Class 時,可以很明顯看出,useLayoutEffect()
的 Cleanup 會在 getSnapshotBeforeUpdate()
和 componentDidUpdate()
之間被調用,而 useLayoutEffect()
則是和 componentDidUpdate()
同級,按照更新過程的順序被調用。
Hooks 做爲子組件時也是這麼個過程,只是沒有了子組件,看上去不那麼明顯罷了。
而 useEffect()
就不同,它和它的 Cleanup 緊密團結在一塊兒,每次執行都是先後腳一塊兒的,從不分離。
不管是類組件仍是 Hooks,單拎出來你們確定都很熟悉它們的生命週期,但當把它們混在一塊兒,就沒那麼簡單了。撰寫這篇博客的過程,幫助我理清了這通亂麻,希望也可以幫到堅持看到這裏的你。