React16 生命週期函數深刻淺出

本篇博文基於 React 16.5.2javascript

吐槽:

做爲一個後端開發,15年開始關注大前端發展趨勢,於17年去線下聽了場前端開發會議,那個時候Vue2.0剛出沒多久,就被那快速構建頁面給吸引了。最先重返前端仍是大半年前,新項目用vue寫了幾個功能頁面,發現如今寫前端是真挺舒服,尤爲是對於後端人員來講(排除掉CSS),快速入門並上手不是什麼問題。html

至於爲何最終選擇了react而非vue?是由於當時對react和vue及RN和weex作了番調研對比,介於weex的不給力,及後期react和vue學習成本差很少,但react的社區更爲活躍,外加發起者背景,就毅然選擇了react。(我我的是通一精百的支持者,因此對於react的理念(learn once,write anywhere),是很同意的。而weex的理念(write once,run anywhere)雖然很吸引人,但時下我的以爲技術並未達到此程度,配置的複雜度及大量的輪子需造,難以知足大型項目的要求。包括目前JD推出的Taro,我的目前持觀望態度,等到react這塊應用到項目以後,再碼一波Taro實際調研一番)。前端

接觸React的時候已是React 16.3,不由感慨前端發展至今,越有後端的趨勢。先後花了3個多月的時間過了一遍webpack4,npm,react,在公司內部作了幾場培訓,發現了其中的一些不協調,但隨着版本的迭代,這些不協調也依次在被更正。(看React17的更新內容,本來一些摸棱兩可的方法、屬性元素命名均會獲得改善:)vue

後續會以此文爲契機,開一個專欄,記錄、分享公司現有電商項目逐步遷移至react技術棧。java

正文

本篇博文會對React16.5.2中的經常使用生命週期函數作一些翻譯、講解說明及發表一些我的的理解和見解,若有錯誤、歧義之處,還望你們不吝嗇指出,互相交流:)react

開篇借用官方提供的生命週期函數圖:(雖然是React 16.4的,但一樣適用於16.5版本) webpack

http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
出處: projects.wojtekmaj.pl/react-lifec…

react組件生命週期函數說明官方地址:reactjs.org/docs/react-…web

getDerivedStateFromProps()方法的16.3版本與以後的16.4和16.5有所調整:npm

  • 在16.3版本上,該方法僅在props變更時纔會被觸發。canvas

  • 在16.3以後的版本上,該方法調整爲props和state的變更都會觸發該方法。

從上圖中,咱們能夠看到,React的生命週期函數主要集中在三個階段:掛載階段(Mounting),更新階段(Updating)和卸載階段(Unmounting)。

其中:

  • Mounting階段主要有這幾個方法須要注意:
    • constructor(props)
    • componentWillReceiveProps(nextProps)
    • getDerivedStateFromProps(props, state)
    • componentWillMount()
    • render()
    • componentDidMount()
  • Updating階段主要有這幾個方法須要注意:
    • componentWillReceiveProps(nextProps)
    • getDerivedStateFromProps(props, state)
    • shouldComponentUpdate()
    • componentWillUpdate()
    • render()
    • componentDidUpdate()
  • Unmounting階段只有一個方法須要注意:
    • componentWillUnmount()
  • 額外的幾個方法說明:
    • componentDidCatch()
    • this.setState()
    • this.forceUpdate()

下面咱們就對以上生命週期函數作進一步說明:

Mounting:

constructor(props)

  • 在構造函數中,通常就作兩件事情:
    1. 初始化state
      • 在構造函數中初始化state需直接賦值,不能調用 this.setState() 方法。
      • constructor()是惟一可以讓你直接給 this.state 賦值的地方,可能有同窗會說能夠在構造函數外(跟constructor同級)直接給 state 賦值,確實,由於 state 派生自 Component 或者 PureComponent 模塊,在外層直接賦值,實際上是在給基類中的 state 賦值,而在 constructor() 使用 this.state = {...} 進行初始化,實際指的是當前組件這個實例。如下兩種賦值方法都可:
        class TestContainer extends PureComponent {
        
            state = {
                content:"1"     //先執行
            }
        
            constructor(props){
                super(props);
                this.state={
                    content:"2" //後執行,最終 this.state.content 爲 2
                }
            }
            。。。
        }
        複製代碼
    2. 綁定方法:this.handleClick = this.handleClick.bind(this);
      • 我所認爲的是:在構造函數中進行方法的綁定後,在使用的時候能夠減小匿名方法的產生,進而提升性能。
  • 若是不須要初始化state,或者綁定方法,那麼能夠不用實現constructor(props)。
  • 若是實現了這個方法,那麼應當在方法體的最開始調用 super(props),不然會致使 this.props 爲 undefined 的問題。
  • 不該在該方法中進行"事件的訂閱/引入有side-effects的方法"。(這種應該放在 componentDidMount() 中)

這裏的 side-effects 方法,翻譯出來是 "反作用" 的意思,我的以爲不妥,有點生澀,可能翻譯成"附加做用"更爲穩當。

它指的是,render()方法應該只完成它的主要功能(如初始化state和綁定方法),不該順帶完成其餘的附加功能,如統計,動畫等。

componentWillReceiveProps(nextProps)

該方法的應用場景:根據特定的props變換,來觸發state的更新。如咱們可能會有一個canvas用來表示頁面loading百分比,這個時候就能夠根據傳入的nextProps中的百分比屬性跟當前state的百分比屬性作對比,若不一致,則更新,以下:

componentWillReceiveProps(nextProps) {
        if (this.props.percent !== nextProps.percent) {
            this.setUpCircle(nextProps.percent);
        }
    }
複製代碼
  • 該方法在React16版本中已經被標記爲unsafe,雖然目前依然可使用(即便用 componentWillReceiveProps 或者 UNSAFE_componentWillReceiveProps 都可),但在 React17 中將會移除,再也不建議使用【該方法將會被 getDerivedStateFromProps() 取代。】
  • 在組件對新(下一個) props 執行任何操做以前,該方法會被觸發,並將 下一個props 做爲參數。
  • 初始渲染時該方法不會被觸發。
  • 能夠在該方法內調用 this.setState(),但並不建議調用,由於這會致使只要組件更新,這個方法就會被執行。

getDerivedStateFromProps(props, state)

該方法的應用場景:使組件可以根據父組件傳入的 props 來更新其自身的 state

在16.4版本以前,只有props更新纔會觸發該方法;但FB在16.4版本作了完善,目前 props 和 state 的更新均會觸發該方法。

  • 該方法的兩個入參,分別表示:
    • props:父組件傳入的值。可能重命名爲 nextProps 更爲直觀。
    • state:組件自身的state,至關於 this.state,可能重命名爲 prevState 更好理解。
  • 該方法在調用 render() 方法前被觸發。
  • 若是父組件的state進行了更新,那麼其子組件也會觸發 getDerivedStateFromProps() 方法。
  • 使用該方法的時候須要初始化 state,不然在控制檯中會出現警告信息,以下圖:
  • 使用該方法,須要在該方法中返回一個:對象/null:
    • 若是返回的是對象,則會更新 state。
    • 若是返回的是null,則表示不更新。
  • 該方法用於取代:componentWillReceiveProps(nextProps) 生命週期函數。
    • getDerivedStateFromProps(props, state) 和 componentWillReceiveProps(nextProps) 的差別:
      • componentWillReceiveProps(props, state):僅在父組件更新時觸發。
      • getDerivedStateFromProps(props, state):除了父組件更新,其自身的state更新時也會觸發。
  • 該方法沒法訪問組件的實例,換句話說,不能在該方法內部,調用 this.state,this.myFunction() 等實例對象/方法。

componentWillMount()

使用場景:能夠在根組件的componentWillMount中作一些App的配置。

  • 該方法在React16版本中已經被標記爲unsafe,雖然目前依然可使用(即便用 componentWillMount 或者 UNSAFE_componentWillMount 都可),但在 React17 中將會移除,再也不建議使用
  • 該方法將會被 getDerivedStateFromProps() 取代。
  • 該方法是SSR(server-side render)渲染上惟一的生命週期函數。
  • 不要在該方法中使用 rpc 請求加載組件的數據。
  • 不能在該方法中調用 this.setState()。

render()

  • 組件中惟一必須的方法。

  • render()方法的返回類型目前有 string,number,boolean,null,portal 以及 fragments(帶有key屬性的數組),eg:

    render() {
            return "string";    //string
            return 123;         //number
            return true;        //若是返回的是false,則什麼都不渲染
            return null;        //若是返回的是null,則什麼都不渲染
            return ReactDOM.createPortal(<MyComponent></MyComponent>, domNode);//portal
            return [            
                <div key="unique_1">若是返回的是數組類型</div>,
                <span key="unique_2">須要在每一個html元素上加上 key 值</span>,     //fragments
                <p key="unique_3">不然控制檯會報錯</p>
            ]
        }
    複製代碼
  • 須要注意的是,組件在更新的時候,也會觸發 render() 方法,但若是 shouldComponentUpdate() 返回的是false,則在"更新階段",render()方法不會被觸發。

  • 該方法應爲一個純函數,除了作渲染的動做外,不該順帶完成其餘動做(如 setState 等),即應避免 side-effects。

  • 應儘可能僅在 render() 這個生命週期函數中中從 this.props 和 this.state 中讀取數據。

componentDidMount()

使用場景:總體來講就是作「獲取一些數據」,或者作必需要有 DOM 才能作的設置。

  • 只會觸發一次:在組件被掛載到頁面以後被觸發,以後就不會再執行了(如頁面的更新等都不會再觸發,需注意)。
  • 通常一些 rpc 請求會放到這個方法中進行調用。
  • 通常事件的訂閱會寫在該方法中(可是不要忘記在組件unmount的時候進行取消訂閱。)
  • 一些須要初始化DOM節點的操做能夠放在這個聲明周期函數中進行。
  • 能夠在該方法中使用 this.setState()。但須要注意,該操做會在瀏覽器更新屏幕以前發生,對於用戶而言是無感的(即在該方法中又額外調用了一次this.setState,致使render()觸發了2次,但用戶的感覺依然是屏幕更新了一次)。不過要謹慎在該方法中調用 this.setState,會致使性能問題。

Mounting 階段,生命週期函數執行順序以下圖(包含子組件)

排除了 UNSAFE 的方法。

Updating:

componentWillReceiveProps(nextProps)

getDerivedStateFromProps(props, state)

shouldComponentUpdate(nextProps, nextState)

使用場景:通常用於精確控制組件是否須要從新渲染(這個方法通常用於性能優化),在絕大多數狀況下,每次 state 的更改都應該讓組件從新渲染。

  • 只要有一個字段進行了更新,那麼其餘全部字段都會進行更新,這個會減慢頁面速度。(經過shouldComponentUpdate,容許咱們只有當咱們關心的 props 更新的時候,才進行組件的更新)。
  • 但須要謹慎使用,由於一旦你本身忘記實現了這個方法,可能會致使你的react組件沒法正常更新。
  • 該方法返回的數據類型是一個boolean,用於決定組件是否應該更新。
    • 默認返回 true。
    • 當返回 false 的時候,阻止的是當前的組件進行更新,對於當前組件的"子組件",並不受影響。
  • 不該該在繼承於 PureComponent 的組件中顯示實現 shouldComponentUpdate(),控制檯會有警告。
  • 目前,當該方法返回false的時候,componentWillUpdate()、更新時的 render() 以及 componentDidUpdate() 都將不會被觸發。(FB在官網上說後期有可能會調整該方法:即便返回 false,依然有可能會觸發再次渲染,需留個心)
  • 何時該方法不會被觸發?
    • 組件第一次渲染時。
    • 在組件中調用 this.forceUpdate() 時。
  • 我的建議:
    • 該方法應儘可能不去手動實現。
    • 不該使用該方法來作一些防止組件渲染的操做,這可能會致使一些莫名的bug,如組件明明應該更新,可是卻沒有更新。
    • 優先能夠考慮繼承 PureComponent 而非 Component 來優化組件而不是手動實現shouldComponentUpdate。
  • 不能在這個方法中使用 this.setState()。

componentWillUpdate(nextProps, nextState)

使用場景:當你實現了 shouldComponentUpdate (返回 true時) 而且在 props 更改時須要執行某些操做的時候,那麼 componentWillUpdate 這個方法仍是有點意義的,但我的認爲,做用不是太大,反卻是增長了理解的複雜度,被刪除也是情理之中。

  • 該方法在React16版本中已經被標記爲unsafe,雖然目前依然可使用(即便用 componentWillUpdate 或者 UNSAFE_componentWillUpdate() 都可),但在 React17 中將會移除,再也不建議使用
  • 從功能上來講,componentWillUpdate(nextProps, nextState) 跟 componentWillReceiveProps(nextProps) 基本相同,只是前者不容許調用 this.setState()。可是,這2個方法
  • 當 shouldComponentUpdate 返回 false時, 該生命週期函數將不會被觸發。
  • 不能使用setState。

render()

componentDidUpdate(prevProps, prevState, snapshot)

使用場景:

  • 通常在這個方法中進行 RPC 請求。(能夠比較下先前的 props 和當前的 props 是否一致,若一致,則能夠不用請求網絡數據。)
  • 若是想要在DOM自身更新以後作一些動做(如從新排列網格等),那麼能夠在這個方法中進行。
  • 該生命週期函數在組件更新完成後當即執行。組件第一次渲染的時候,該生命週期函數不會被觸發。
  • 能夠作跟 componentDidMount 中所做的相同的事情:如重置佈局,重繪畫布等
  • 可使用setState。

Updating 階段,生命週期函數執行順序以下圖(包含子組件)

排除了 UNSAFE 的方法。

Unmounting:

componentWillUnmount()

  • 該方法在組件將要被卸載的時候觸發。
  • 另外,在渲染期間,當前組件/其子組件的componentDidMount()函數發生錯誤時,該方法也將被觸發。
  • eg:
    • 我刻意在"嵌套在子組件中的組件"中的componentDidMount()方法中,拋出了一個異常,此時該組件往上的父級組件均觸發了 componentWillUnmount()
      • 我嘗試在construct(),componentDidUpdate()等生命週期函數中拋出異常,均不會觸發 componentWillUnmount()。

Unmounting 階段,生命週期函數執行順序以下圖(包含子組件)

這裏須要注意下,該函數是 will unmount,因此觸發順序上是從父組件到子組件,但釋放順序上,是最內層的子組件先釋放,最終最外層的根組件才釋放。

額外的幾個方法說明:

componentDidCatch(error,info)

使用場景:UI中的一些JS錯誤不該該使整個App崩潰,爲了解決這個問題,React16中引入了Error Boundary(錯誤邊界)這個概念,旨在解決容許頁面的部分組件異常但不影響App的渲染。能夠認爲是組件中的 try-catch。而爲了實現這個功能,就須要藉助 componentDidCatch() 這個生命週期函數。

  • Error Boundaris是自定義的 react 的組件,這些組件能夠捕獲其子組件的異常,並顯示子組件錯誤時的替代組件內容,而App不會崩潰。
  • 怎麼樣的組件能夠認爲是 Error Boundary 組件?
    • 只要該組件內部實現了 componentDidCatch 這個方法就能夠認爲是 ErrorBoundary 組件,如:

      ...
      class MyErrorBoundary extends Component{
          ...
          componentDidCatch(error,info){
              ...
          }
          ...
      }
      複製代碼
  • 一個例子:當子組件發生錯誤時,顯示"sth wrong here."
    • MyErrorBoundary:

      import React, { Component } from 'react';
      class MyErrorBoundary extends Component {
          state = {
              isError: false
          }
          render() {
              if (this.state.isError) {
                  return (<div>sth wrong here.</div>);
              }
              return this.props.children;
          }
          componentDidCatch(error, info) {
              console.log(error, info);
              this.setState({
                  isError = true
              });
              //也能夠作其餘的一些事情,如日誌,異常數統計等
          }
      }
      export default MyErrorBoundary;
      複製代碼
    • App.js

      ...
      render(){
          return (
          ...
          <MyErrorBoundary>
              <OtherComponent /> </MyErrorBoundary>
          ...
          );
      }
      ...
      複製代碼
  • ErrorBoundary 組件只能捕獲其子組件,沒法捕獲其自身的異常。
  • 爲何用 ErrorBoundary 這樣的組件?直接在代碼中 try-catch 不是很直觀?
    • 這個問題,就仁智各見了,try-catch 面向的是 代碼,而 ErrorBoundary 面向的是組件:這裏我想用一個後端例子來講明,在寫後端代碼時(好比一個 Web Api) ,你能夠在具體業務代碼中使用 try-catch 來對可能出現異常的代碼塊作處理,但有的時候,你可能想針對全部「方法」作異常處理,這個時候使用 AOP 的方式來寫一個異常處理的方法更好(或者能夠認爲是 Exception filters這樣的自定義類)
  • 注意事項:若要查看ErrorBoundary的效果,沒法在開發模式下進行(即mode=development)【或者直接運行 npm start,是不會出效果的,會顯示具體的錯誤信息。】,有兩種方法能夠解決:
      1. 使用 npm run build命令將項目打包後發佈到服務器上進行;
      1. 將項目的模式從development調整爲 production
      • 需注意,如果使用create-react-app來建立的項目,配置文件須要解包後才能進行編輯,運行命令: npm run eject,解包後調整"/scripts/build.js",將 process.env.NODE_ENV 賦值爲 production,以後再運行 npm start 的時候就能看到上面示例代碼的效果。

this.setState()

  • 用來設置state的值。

  • 在16.3開始,FB建議使用setState的異步函數寫法,如: 原先咱們在使用的時候是直接進行賦值:

    inputHandler = (e) => {
        this.setState({
            inputValue: e.target.value              //再也不這麼作
        });
    )
    複製代碼

而是改用:

```javascript
    inputHandler = (e) => {
        let value = e.target.value;
        this.setState(() => {
            return {inputValue: value};   //使用方法的形式,最終再返回一個對象,這裏須要注意下,這麼寫是異步的,但存在一個問題,即輸入的內容,須要再方法外層先獲取到:如這裏的value。
        })
    }
    ```
複製代碼
  • 異步寫法中,該方法提供了一個回調函數,經過該回調函數,能夠確保只有等到setState觸發完成以後,纔會執行回調的方法(另一個能夠確保在setState執行完成以後再執行的點是componentDidUpdate方法),如:

    this.setState((prevState, nextState)=>{
        ...    
    }, callback);
    複製代碼
    • 該方法提供了2個入參,prevState 和 props,前者至關因而 this.state。

    • 只有當setState方法的第一個參數執行完成以後,纔會執行 callback方法。

    • 須要注意的是,由於setState是一個異步方法,因此在賦值的時候須要注意下,若是須要從表單或者其餘地方獲取值賦值給state的某一個屬性,須要先把這個值在setState方法以前賦給一個變量,再在setState方法中使用這個變量。如

      let name = e.target.value;
          this.setState((prevState, props)=>{
              return {
                  userName : name  //不可以直接 userName:e.target.value,異步方法中獲取不到當前上下文
              }    
          }, callback);
      複製代碼
  • 補充:對於 setState ,其不必定在調用 setState 的時候就當即觸發這個動做。爲了性能,react會自行判斷,將組件的全部setState在同一個時間點一同執行(批量執行),而非調用一次就執行一次。

  • 每一次 setState 都會致使組件的再次渲染,除非 shouldComponentUpdate 返回 false。

this.forceUpdate()

  • 調用forceUpdate()的時候,將會跳過 shouldComponentUpdate()而直接從新render()組件。
  • 父組件中調用 forceUpdate 亦會致使子組件的生命週期函數被觸發(包括componentDidUpdate)。
  • 正常狀況下,這個方法不多使用。
相關文章
相關標籤/搜索