天天看點

Hooks 與 React 生命周期

一、Hooks 元件

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

但是引入 Hooks 之後就變得不同了,它能讓元件在不使用 class 的情況下擁有 state,是以就有了生命周期的概念,所謂的生命周期其實就是 

useState

、 

useEffect()

 和 

useLayoutEffect()

 。

即:Hooks 元件(使用了Hooks的函數元件)有生命周期,而函數元件(未使用Hooks的函數元件)是沒有生命周期的。

下面,是具體的 class 與 Hooks 的生命周期對應關系:

為友善記憶,大緻彙總成表格如下。

class 元件 Hooks 元件
constructor useState
getDerivedStateFromProps useState 裡面 update 函數
shouldComponentUpdate useMemo
render 函數本身
componentDidMount useEffect
componentDidUpdate useEffect
componentWillUnmount useEffect  裡面傳回的函數
componentDidCatch
getDerivedStateFromError

二、單個元件的生命周期

1. 生命周期

V16.3 之前

我們可以将生命周期分為三個階段:

分開來講:

  1. 挂載階段
  1. 元件更新階段
  1. 解除安裝階段
Hooks 與 React 生命周期

這種生命周期會存在一個問題,那就是當更新複雜元件的最上層元件時,調用棧會很長,如果在進行複雜的操作時,就可能長時間阻塞主線程,帶來不好的使用者體驗,Fiber 就是為了解決該問題而生。

V16.3 之後

Fiber 本質上是一個虛拟的堆棧幀,新的排程器會按照優先級自由排程這些幀,進而将之前的同步渲染改成了異步渲染,在不影響體驗的情況下去分段計算更新。

對于異步渲染,分為兩階段:

其中,

reconciliation

 階段是可以被打斷的,是以 

reconcilation

 階段執行的函數就會出現多次調用的情況,顯然,這是不合理的。

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

  1. static getDerivedStateFromProps

    :該函數在挂載階段群組件更新階段都會執行,即每次擷取新的

    props

     或 

    state

    之後都會被執行,在挂載階段用來代替

    componentWillMount

    ;在元件更新階段配合 

    componentDidUpdate

    ,可以覆寫 

    componentWillReceiveProps

     的所有用法。

    同時它是一個靜态函數,是以函數體内不能通路 

    this

    ,會根據 

    nextProps

     和 

    prevState

     計算出預期的狀态改變,傳回結果會被送給 

    setState

    ,傳回 

    null

     則說明不需要更新 

    state

    ,并且這個傳回是必須的。
  2. getSnapshotBeforeUpdate

    : 該函數會在 

    render

     之後, DOM 更新前被調用,用于讀取最新的 DOM 資料。

    傳回一個值,作為 

    componentDidUpdate

     的第三個參數;配合 

    componentDidUpdate

    , 可以覆寫

    componentWillUpdate

    的所有用法。

注意:V16.3 中隻用在元件挂載或元件 

props

 更新過程才會調用,即如果是因為自身 setState 引發或者forceUpdate 引發,而不是由父元件引發的話,那麼

static getDerivedStateFromProps

也不會被調用,在 V16.4 中更正為都調用。

即更新後的生命周期為:

  1. 挂載階段
  1. 更新階段
  1. 解除安裝階段
Hooks 與 React 生命周期

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>      </>    );  }}
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      });    }  }  // ...}
  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} />;}
  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}/>
  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 };}
 super(props);
 // 千萬不要這樣做
 // 直接用 props,保證單一資料源
 this.state = { phone: props.phone };
}
           

三、多個元件的執行順序

1. 父子元件

  1. static getDerivedStateFromProps

  2. shouldComponentUpdate

第 二 階段,此時 DOM 節點已經生成完畢,元件挂載完成,開始後續流程。先依次觸發同步子元件以下函數,最後觸發父元件的。

React 會按照上面的順序依次執行這些函數,每個函數都是各個子元件的先執行,然後才是父元件的執行。

是以執行順序是:

父元件 getDerivedStateFromProps —> 父元件 shouldComponentUpdate —> 子元件 getDerivedStateFromProps —> 子元件 shouldComponentUpdate —> 子元件 getSnapshotBeforeUpdate —>  父元件 getSnapshotBeforeUpdate —> 子元件 componentDidUpdate —> 父元件 componentDidUpdate

  1. getSnapshotBeforeUpdate()

  2. componentDidUpdate()

解除安裝階段

componentWillUnmount()

,順序為 父元件的先執行,子元件按照在 JSX 中定義的順序依次執行各自的方法。

注意 :如果解除安裝舊元件的同時伴随有新元件的建立,新元件會先被建立并執行完 

render

,然後解除安裝不需要的舊元件,最後新元件執行挂載完成的回調。