Hooks 與 React 生命週期的關係

你真的瞭解 React 生命週期嗎?

React 生命週期不少人都瞭解,但一般咱們所瞭解的都是 單個組件 的生命週期,但針對 Hooks 組件、多個關聯組件(父子組件和兄弟組件) 的生命週期又是怎麼樣的喃?你有思考和了解過嗎,接下來咱們將完整的瞭解 React 生命週期。react

關於 組件 ,咱們這裏指的是 React.Component 以及 React.PureComponent ,可是否包括 Hooks 組件喃?算法

1、Hooks 組件

函數組件 的本質是函數,沒有 state 的概念的,所以不存在生命週期一說,僅僅是一個 render 函數而已。數組

可是引入 Hooks 以後就變得不一樣了,它能讓組件在不使用 class 的狀況下擁有 state,因此就有了生命週期的概念,所謂的生命週期其實就是 useStateuseEffect()useLayoutEffect()瀏覽器

即:Hooks 組件(使用了Hooks的函數組件)有生命週期,而函數組件(未使用Hooks的函數組件)是沒有生命週期的安全

下面,是具體的 class 與 Hooks 的生命週期對應關係app

  • constructor:函數組件不須要構造函數,咱們能夠經過調用 useState 來初始化 state。若是計算的代價比較昂貴,也能夠傳一個函數給 useStatedom

    const [num, UpdateNum] = useState(0)
  • getDerivedStateFromProps:通常狀況下,咱們不須要使用它,咱們能夠在渲染過程當中更新 state,以達到實現 getDerivedStateFromProps 的目的。異步

    function ScrollView({row}) {
      let [isScrollingDown, setIsScrollingDown] = useState(false);
      let [prevRow, setPrevRow] = useState(null);
    
      if (row !== prevRow) {
        // Row 自上次渲染以來發生過改變。更新 isScrollingDown。
        setIsScrollingDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
      }
    
      return `Scrolling down: ${isScrollingDown}`;
    }

    React 會當即退出第一次渲染並用更新後的 state 從新運行組件以免耗費太多性能。函數

  • shouldComponentUpdate:能夠用 React.memo 包裹一個組件來對它的 props 進行淺比較工具

    const Button = React.memo((props) => {
      // 具體的組件
    });

    注意:React.memo 等效於 PureComponent,它只淺比較 props。這裏也可使用 useMemo 優化每個節點。

  • render:這是函數組件體自己。
  • componentDidMount, componentDidUpdate: useLayoutEffect 與它們兩的調用階段是同樣的。可是,咱們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffectuseEffect 能夠表達全部這些的組合。

    // componentDidMount
    useEffect(()=>{
      // 須要在 componentDidMount 執行的內容
    }, [])
    
    useEffect(() => { 
      // 在 componentDidMount,以及 count 更改時 componentDidUpdate 執行的內容
      document.title = `You clicked ${count} times`; 
      return () => {
        // 須要在 count 更改時 componentDidUpdate(先於 document.title = ... 執行,遵照先清理後更新)
        // 以及 componentWillUnmount 執行的內容       
      } // 當函數中 Cleanup 函數會按照在代碼中定義的順序前後執行,與函數自己的特性無關
    }, [count]); // 僅在 count 更改時更新

    請記得 React 會等待瀏覽器完成畫面渲染以後纔會延遲調用 useEffect,所以會使得額外操做很方便

  • componentWillUnmount:至關於 useEffect 裏面返回的 cleanup 函數

    // componentDidMount/componentWillUnmount
    useEffect(()=>{
      // 須要在 componentDidMount 執行的內容
      return function cleanup() {
        // 須要在 componentWillUnmount 執行的內容      
      }
    }, [])
  • componentDidCatch and getDerivedStateFromError:目前尚未這些方法的 Hook 等價寫法,但很快會加上。

爲方便記憶,大體彙總成表格以下。

class 組件 Hooks 組件
constructor useState
getDerivedStateFromProps useState 裏面 update 函數
shouldComponentUpdate useMemo
render 函數自己
componentDidMount useEffect
componentDidUpdate useEffect
componentWillUnmount useEffect 裏面返回的函數
componentDidCatch
getDerivedStateFromError

2、單個組件的生命週期

1. 生命週期

V16.3 以前

咱們能夠將生命週期分爲三個階段:

  • 掛載階段
  • 組件更新階段
  • 卸載階段

分開來說:

  1. 掛載階段

    • constructor:避免將 props 的值複製給 state
    • componentWillMount
    • render:react 最重要的步驟,建立虛擬 dom,進行 diff 算法,更新 dom 樹都在此進行
    • componentDidMount
  2. 組件更新階段

    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  3. 卸載階段

    • componentWillUnMount

這種生命週期會存在一個問題,那就是當更新複雜組件的最上層組件時,調用棧會很長,若是在進行復雜的操做時,就可能長時間阻塞主線程,帶來很差的用戶體驗,Fiber 就是爲了解決該問題而生。

V16.3 以後

Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先級自由調度這些幀,從而將以前的同步渲染改爲了異步渲染,在不影響體驗的狀況下去分段計算更新。

對於異步渲染,分爲兩階段:

  • reconciliation

    • componentWillMount
    • componentWillReceiveProps
    • shouldConmponentUpdate
    • componentWillUpdate
  • commit

    • componentDidMount
    • componentDidUpdate

其中,reconciliation 階段是能夠被打斷的,因此 reconcilation 階段執行的函數就會出現屢次調用的狀況,顯然,這是不合理的。

因此 V16.3 引入了新的 API 來解決這個問題:

  1. static getDerivedStateFromProps: 該函數在掛載階段和組件更新階段都會執行,即每次獲取新的propsstate 以後都會被執行在掛載階段用來代替componentWillMount;在組件更新階段配合 componentDidUpdate,能夠覆蓋 componentWillReceiveProps 的全部用法。

    同時它是一個靜態函數,因此函數體內不能訪問 this,會根據 nextPropsprevState 計算出預期的狀態改變,返回結果會被送給 setState返回 null 則說明不須要更新 state,而且這個返回是必須的

  2. getSnapshotBeforeUpdate: 該函數會在 render 以後, DOM 更新前被調用,用於讀取最新的 DOM 數據。

    返回一個值,做爲 componentDidUpdate 的第三個參數;配合 componentDidUpdate, 能夠覆蓋componentWillUpdate 的全部用法。

注意:V16.3 中只用在組件掛載或組件 props 更新過程纔會調用,即若是是由於自身 setState 引起或者forceUpdate 引起,而不是由父組件引起的話,那麼static getDerivedStateFromProps也不會被調用,在 V16.4 中更正爲都調用。

即更新後的生命週期爲:

  1. 掛載階段

    • constructor
    • static getDerivedStateFromProps
    • render
    • componentDidMount
  2. 更新階段

    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  3. 卸載階段

    • componentWillUnmount

2. 生命週期,誤區

誤解一:getDerivedStateFromProps 和 componentWillReceiveProps 只會在 props 改變 時纔會調用

實際上,只要父級從新渲染,getDerivedStateFromProps 和 componentWillReceiveProps 都會從新調用,無論 props 有沒有變化。因此,在這兩個方法內直接將 props 賦值到 state 是不安全的。

// 子組件
class PhoneInput extends Component {
  state = { phone: this.props.phone };

  handleChange = e => {
    this.setState({ phone: e.target.value });
  };

  render() {
    const { phone } = this.state;
    return <input onChange={this.handleChange} value={phone} />;
  }

  componentWillReceiveProps(nextProps) {
    // 不要這樣作。
    // 這會覆蓋掉以前全部的組件內 state 更新!
    this.setState({ phone: nextProps.phone });
  }
}

// 父組件
class App extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    // 使用了 setInterval,
    // 每秒鐘都會更新一下 state.count
    // 這將致使 App 每秒鐘從新渲染一次
    this.interval = setInterval(
      () =>
        this.setState(prevState => ({
          count: prevState.count + 1
        })),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return (
      <>
        <p>
          Start editing to see some magic happen :)
        </p>
        <PhoneInput phone='call me!' /> 
        <p>
          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.
        </p>
      </>
    );
  }
}

實例可點擊這裏查看

固然,咱們能夠在 父組件App 中 shouldComponentUpdate 比較 props 的 email 是否是修改再決定要不要從新渲染,可是若是子組件接受多個 props(較爲複雜),就很難處理,並且 shouldComponentUpdate 主要是用來性能提高的,不推薦開發者操做 shouldComponetUpdate(可使用 React.PureComponet)。

咱們也可使用 在 props 變化後修改 state

class PhoneInput extends Component {
  state = {
    phone: this.props.phone
  };

  componentWillReceiveProps(nextProps) {
    // 只要 props.phone 改變,就改變 state
    if (nextProps.phone !== this.props.phone) {
      this.setState({
        phone: nextProps.phone
      });
    }
  }
  
  // ...
}

但這種也會致使一個問題,當 props 較爲複雜時,props 與 state 的關係很差控制,可能致使問題

解決方案一:徹底可控的組件

function PhoneInput(props) {
  return <input onChange={props.onChange} value={props.phone} />;
}

徹底由 props 控制,不派生 state

解決方案二:有 key 的非可控組件

class PhoneInput extends Component {
  state = { phone: this.props.defaultPhone };

  handleChange = event => {
    this.setState({ phone: event.target.value });
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.phone} />;
  }
}

<PhoneInput
  defaultPhone={this.props.user.phone}
  key={this.props.user.id}
/>

當 key 變化時, React 會建立一個新的而不是更新一個既有的組件

誤解二:將 props 的值直接複製給 state

應避免將 props 的值複製給 state

constructor(props) {
 super(props);
 // 千萬不要這樣作
 // 直接用 props,保證單一數據源
 this.state = { phone: props.phone };
}

3、多個組件的執行順序

1. 父子組件

  • 掛載階段

    兩個 階段:

    • 階段,由父組件開始執行到自身的 render,解析其下有哪些子組件須要渲染,並對其中 同步的子組件 進行建立,按 遞歸順序 挨個執行各個子組件至 render,生成到父子組件對應的 Virtual DOM 樹,並 commit 到 DOM。
    • 階段,此時 DOM 節點已經生成完畢,組件掛載完成,開始後續流程。先依次觸發同步子組件各自的 componentDidMount,最後觸發父組件的。

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

因此執行順序是:

父組件 getDerivedStateFromProps —> 同步子組件 getDerivedStateFromProps —> 同步子組件 componentDidMount —> 父組件 componentDidMount —> 異步子組件 getDerivedStateFromProps —> 異步子組件 componentDidMount

  • 更新階段

    React 的設計遵循單向數據流模型 ,也就是說,數據均是由父組件流向子組件。

    • 階段,由父組件開始,執行

      1. static getDerivedStateFromProps
      2. shouldComponentUpdate
更新到自身的 `render`,解析其下有哪些子組件須要渲染,並對 **子組件** 進行建立,按 **遞歸順序** 挨個執行各個子組件至 `render`,生成到父子組件對應的 Virtual DOM 樹,並與已有的 Virtual DOM 樹 比較,計算出 **Virtual DOM 真正變化的部分** ,並只針對該部分進行的原生DOM操做。
  • 階段,此時 DOM 節點已經生成完畢,組件掛載完成,開始後續流程。先依次觸發同步子組件如下函數,最後觸發父組件的。

    1. getSnapshotBeforeUpdate()
    2. componentDidUpdate()
React 會按照上面的順序依次執行這些函數,每一個函數都是各個子組件的先執行,而後纔是父組件的執行。

因此執行順序是:

父組件 getDerivedStateFromProps —> 父組件 shouldComponentUpdate —> 子組件 getDerivedStateFromProps —> 子組件 shouldComponentUpdate —> 子組件 getSnapshotBeforeUpdate —>  父組件 getSnapshotBeforeUpdate —> 子組件 componentDidUpdate —> 父組件 componentDidUpdate
  • 卸載階段

    componentWillUnmount(),順序爲 父組件的先執行,子組件按照在 JSX 中定義的順序依次執行各自的方法

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

2. 兄弟組件

  • 掛載階段

    如果同步路由,它們的建立順序和其在共同父組件中定義的前後順序是 一致 的。

    如果異步路由,它們的建立順序和 js 加載完成的順序一致。

  • 更新階段、卸載階段

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

    因此,兄弟組件更新、卸載階段,請參考 父子組件

走在最後:走心推薦一個在線編輯工具:StackBlitz,能夠在線編輯 Angular、React、TypeScript、RxJS、Ionic、Svelte項目

預告:後續將加入高階組件的生命週期,敬請期待小瓶子的下次更新。

相關文章
相關標籤/搜索