React 原始碼剖析系列 - 生命週期的管理藝術

發表於2015-11-23

目前,前端領域中 React 勢頭正盛,很少能夠深入剖析內部實現機制和原理。本系列文章希望通過剖析 React 原始碼,理解其內部的實現原理,知其然更要知其所以然。

對於 React,其元件生命週期(Component Lifecycle)是它的核心概念,本文從原始碼入手,來剖析 React 生命週期的管理藝術。

  • 閱讀本文需要對 React 有一定的瞭解,如果你不知何為元件的生命週期,請詳讀 React 生命週期的文件
  • 如果你對 React 元件的生命週期存在些許疑惑,如生命週期如何順序管理;setState 如何實現非同步操作,又是何時真正更新等,那麼本文值得閱讀。

前言

React 的主要思想是通過構建可複用元件來構建使用者介面。所謂元件其實就是 有限狀態機,通過狀態渲染對應的介面,且每個元件都有自己的生命週期,它規定了元件的狀態和方法需要在哪個階段進行改變和執行。

有限狀態機(FSM),表示有限個狀態以及在這些狀態之間的轉移和動作等行為的模型。一般通過狀態、事件、轉換和動作來描述有限狀態機,下面是描述組合鎖狀態機的模型圖,包括5個狀態、5個狀態自轉換、6個狀態間轉換和1個復位 RESET 轉換到狀態 S1。狀態機,能夠記住目前所處的狀態,根據當前的狀態可以做出相應的決策,並且在進入不同的狀態時,可以做不同的操作。通過狀態機將複雜的關係簡單化,利用這種自然而直觀的方式可以讓程式碼更容易理解。

React 正是利用這一概念,通過管理狀態來實現對元件的管理。例如,某個元件有顯示和隱藏兩個狀態,通常會設計兩個方法 show() 和 hide() 來實現切換;而 React 只需要設定狀態 setState({ showed: true/false }) 即可實現。同時,React 還引入了元件的生命週期概念。通過它就可以實現元件的狀態機控制,從而達到 “生命週期-狀態-元件” 的和諧畫面。

雖然元件、狀態機、生命週期這三者都不是 React 獨創,如果熟悉 Web Components 標準,它與其中的自定義元件的生命週期的概念相似。但就目前而言,React 是將這三種概念結合地相對清晰流暢的介面庫。

初探 React 生命週期

在自定義 React 元件時,根據需要會在元件生命週期的不同階段實現不同的邏輯。為了檢視 元件生命週期的執行順序,你可以使用 react-lifecycle mixin,將此 mixin 新增到需要觀察的元件中,當任何生命週期方法被呼叫時,都能在控制檯觀察到對應的生命週期的呼叫時狀態。

通過反覆試驗,得到了元件的生命週期在不同狀態下的執行順序:

  • 當首次裝載元件時,按順序執行 getDefaultProps、getInitialState、componentWillMount、render 和 componentDidMount;
  • 當解除安裝元件時,執行 componentWillUnmount;
  • 當重新裝載元件時,此時按順序執行 getInitialState、componentWillMount、render 和 componentDidMount,但並不執行 getDefaultProps;
  • 當再次渲染元件時,元件接受到更新狀態,此時按順序執行 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。

疑問

  • 為何 React 會按上述順序執行生命週期?
  • 為何 React 多次 render 時,會執行生命週期的不同階段?
  • 為何 getDefaultProps 只執行了1次?

詳解 React 生命週期

自定義元件(ReactCompositeComponent)的生命週期主要通過三種狀態進行管理:MOUNTINGRECEIVE_PROPSUNMOUNTING,它們負責通知元件當前所處的狀態,應該執行生命週期中的哪個步驟,是否可以更新 state。三個狀態對應三種方法,分別為:mountComponentupdateComponentunmountComponent,每個方法都提供了兩種處理方法,will 方法在進入狀態之前呼叫,did 方法在進入狀態之後呼叫,三種狀態三種方法五種處理方法,此外還提供兩種特殊狀態的處理方法。

  • mountComponent -> MOUNTING
  • updateComponent -> RECEIVE_PROPS
  • unmountComponent -> UNMOUNTING

createClass 建立自定義元件

createClass 建立自定義元件的入口方法,負責管理生命週期中的 getDefaultProps。getDefaultProps 方法只執行一次,這樣所有例項初始化的 props 將會被共享。

通過 createClass 建立自定義元件,利用原型繼承 ReactCompositeComponentBase 父類,按順序合併 mixins,設定初始化 defaultProps,建立元素 ReactElement。

 

狀態一:MOUNTING

mountComponent 負責管理生命週期中的 getInitialState、componentWillMount、render 和 componentDidMount

由於 getDefaultProps 是通過 Constructor 進行管理,因此也是整個生命週期中最先開始執行,而 mountComponent 只能望洋興嘆,無法呼叫到 getDefaultProps。這就解釋了為何 getDefaultProps 只執行1次的原因

由於通過 ReactCompositeComponentBase 返回的是一個虛擬節點,因此需要利用 instantiateReactComponent 去得到例項,再使用 mountComponent 拿到結果作為當前自定義元素的結果。

首先通過 mountComponent 裝載元件,此時,將狀態設定為 MOUNTING,利用 getInitialState 獲取初始化 state,初始化更新佇列。

若存在 componentWillMount,則執行;如果此時在 componentWillMount 中呼叫 setState,是不會觸發 reRender,而是進行 state 合併。

到此時,已經完成 MOUNTING 的工作,更新狀態為 NULL,同時 state 也將執行更新操作,此刻在 render 中可以獲取更新後的 this.state 資料。

其實,mountComponent 本質上是通過 遞迴渲染 內容的,由於遞迴的特性,父元件的 componentWillMount 一定在其子元件的 componentWillMount 之前呼叫,而父元件的 componentDidMount 肯定在其子元件的 componentDidMount 之後呼叫。

當渲染完成之後,若存在 componentDidMount 則觸發。這就解釋了 componentWillMount – render – componentDidMount 三者之間的執行順序。

instantiateReactComponent 通過判斷元素型別(型別包括:object、string、function)建立元素例項,這裡不做過多介紹,當講解到 React Virtual DOM 時,再詳細介紹此方法。

 

狀態二:RECEIVE_PROPS

updateComponent 負責管理生命週期中的 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate

首先通過 updateComponent 更新元件,如果前後元素不一致說明需要進行元件更新,此時將狀態設定為RECEIVING_PROPS

若存在 componentWillReceiveProps,則執行;如果此時在 componentWillReceiveProps 中呼叫 setState,是不會觸發 reRender,而是進行 state 合併。

到此時,已經完成 RECEIVING_PROPS 工作,更新狀態為 NULL,同時 state 也將執行更新操作,此刻 this.state 可以獲取到更新後的資料。

注意:此時 this.state 雖然獲取到更新資料,但只能在內部原始碼中使用,我們在開發時,若在 componentWillReceiveProps 中呼叫 setState,那麼在 componentWillReceiveProps、shouldComponentUpdate 和 componentWillUpdate 中還是無法獲取到更新後的 this.state,即此時訪問的this.state 仍然是未更新的資料,只有在 render 和 componentDidUpdate 中才能獲取到更新後的this.state。

呼叫 shouldComponentUpdate 判斷是否需要進行元件更新,如果存在 componentWillUpdate,則執行。

updateComponent 本質上也是通過 遞迴渲染 內容的,由於遞迴的特性,父元件的 componentWillUpdate 一定在其子元件的 componentWillUpdate 之前呼叫,而父元件的 componentDidUpdate 肯定在其子元件 componentDidUpdate 之後呼叫。

當渲染完成之後,若存在 componentDidUpdate,則觸發,這就解釋了 componentWillReceiveProps – componentWillUpdate – render – componentDidUpdate 它們之間的執行順序。

注意:禁止在 shouldComponentUpdatecomponentWillUpdate 中呼叫 setState,會造成迴圈呼叫,直至耗光瀏覽器記憶體後崩潰。(請繼續閱讀,尋找答案)

狀態三:UNMOUNTING

unmountComponent 負責管理生命週期中的 componentWillUnmount

首先將狀態設定為 UNMOUNTING,若存在 componentWillUnmount,則執行;如果此時在 componentWillUnmount 中呼叫 setState,是不會觸發 reRender。更新狀態為 NULL,完成元件解除安裝操作。

 

setState 更新機制

當呼叫 setState 時,會對 state 以及 _pendingState 更新佇列進行合併操作,但其實真正更新 state 的幕後黑手是replaceState

replaceState 會先判斷當前狀態是否為 MOUNTING,如果不是即會呼叫 ReactUpdates.enqueueUpdate 執行更新。

當狀態不為 MOUNTINGRECEIVING_PROPS 時,performUpdateIfNecessary 會獲取 _pendingElement、_pendingState、_pendingForceUpdate,並呼叫 updateComponent 進行元件更新。

如果在 shouldComponentUpdate 或 componentWillUpdate 中呼叫 setState,此時的狀態已經從 RECEIVING_PROPS -> NULL,則 performUpdateIfNecessary 就會呼叫 updateComponent 進行元件更新,但 updateComponent 又會呼叫 shouldComponentUpdate 和 componentWillUpdate,因此造成迴圈呼叫,使得瀏覽器記憶體佔滿後崩潰。

總結

  • React 通過三種狀態:MOUNTINGRECEIVE_PROPSUNMOUNTING,管理整個生命週期的執行順序;
  • setState 會先進行 _pendingState 更新佇列的合併操作,不會立刻 reRender,因此是非同步操作,且通過判斷狀態(MOUNTINGRECEIVE_PROPS)來控制 reRender 的時機;
  • 不建議在 getDefaultProps、getInitialState、shouldComponentUpdate、componentWillUpdate、render 和 componentWillUnmount 中呼叫 setState,特別注意:不能在 shouldComponentUpdate 和 componentWillUpdate中呼叫 setState,會導致迴圈呼叫。

參考資料

如果本文能夠為你解決些許關於 React 生命週期管理的疑惑,請點個贊吧!

相關文章