談談對 React 新舊生命週期的理解

前言

在寫這篇文章的時候,React 已經出了 17.0.1 版本了,雖然說還來討論目前 React 新舊生命週期有點晚了,React 兩個新生命週期雖然出了好久,但實際開發我卻沒有用過,由於 React 16 版本後咱們直接 React Hook 起飛開發項目。javascript

但對新舊生命週期的探索,仍是有助於咱們更好理解 React 團隊一些思想和作法,因而今天就要回顧下這個問題和理解總結,雖然仍是 React Hook 寫法香,可是依然要深究學習類組件的東西,瞭解 React 團隊的一些思想與作法。html

本文只討論 React17 版本前的。java

React 16 版本後作了什麼

首先是給三個生命週期函數加上了 UNSAFE:react

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

這裏並非表示不安全的意思,它只是不建議繼續使用,並表示使用這些生命週期的代碼可能在將來的 React 版本(目前 React17 尚未徹底廢除)存在缺陷,如 React Fiber 異步渲染的出現。git

同時新增了兩個生命週期函數:github

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

UNSAFE_componentWillReceiveProps

UNSAFE_componentWillReceiveProps(nextProps)

先來講說這個函數,componentWillReceiveProps編程

該子組件方法並非父組件 props 改變才觸發,官方回答是:安全

若是父組件致使組件從新渲染,即便 props 沒有更改,也會調用此方法。若是隻想處理更改,請確保進行當前值與變動值的比較。

先來講說 React 爲何廢除該函數,廢除確定有它很差的地方。服務器

componentWillReceiveProps函數的通常使用場景是:異步

  • 若是組件自身的某個 state 跟父組件傳入的 props 密切相關的話,那麼能夠在該方法中判斷先後兩個 props 是否相同,若是不一樣就根據 props 來更新組件自身的 state。
    相似的業務需求好比:一個能夠橫向滑動的列表,當前高亮的 Tab 顯然隸屬於列表自身的狀態,但不少狀況下,業務需求會要求從外部跳轉至列表時,根據傳入的某個值,直接定位到某個 Tab。

但該方法缺點是會破壞 state 數據的單一數據源,致使組件狀態變得不可預測,另外一方面也會增長組件的重繪次數。

而在新版本中,官方將更新 state 與觸發回調從新分配到了 getDerivedStateFromPropscomponentDidUpdate 中,使得組件總體的更新邏輯更爲清晰。

新生命週期方法static getDerivedStateFromProps(props, state)怎麼用呢?

getDerivedStateFromProps 會在調用 render 方法以前調用,而且在初始掛載及後續更新時都會被調用。它應返回一個對象來更新 state,若是返回 null 則不更新任何內容。

從函數名字就能夠看出大概意思:使用 props 來派生/更新 state。這就是重點了,但凡你想使用該函數,都必須出於該目的,使用它纔是正確且符合規範的。

getDerivedStateFromProps不一樣的是,它在掛載和更新階段都會執行(componentWillReceiveProps掛載階段不會執行),由於更新 state 這種需求不只在 props 更新時存在,在 props 初始化時也是存在的。

並且getDerivedStateFromProps在組件自身 state 更新也會執行而componentWillReceiveProps方法執行則取決於父組件的是否觸發從新渲染,也能夠看出getDerivedStateFromProps並非 componentWillReceiveProps方法的替代品.

引發咱們注意的是,這個生命週期方法是一個靜態方法,靜態方法不依賴組件實例而存在,故在該方法內部是沒法訪問 this 的。新版本生命週期方法能作的事情反而更少了,限制咱們只能根據 props 來派生 state,官方是基於什麼考量呢?

由於沒法拿到組件實例的 this,這也致使咱們沒法在函數內部作 this.fetch()請求,或者不合理的 this.setState()操做致使可能的死循環或其餘反作用。有沒有發現,這都是不合理不規範的操做,但開發者們都有機會這樣用。可若是加了個靜態 static,間接強制咱們都沒法作了,也從而避免對生命週期的濫用。

React 官方也是經過該限制,儘可能保持生命週期行爲的可控可預測,根源上幫助了咱們避免不合理的編程方式,即一個 API 要保持單一性,作一件事的理念。

以下例子:

// before
componentWillReceiveProps(nextProps) {
  if (nextProps.isLogin !== this.props.isLogin) {
    this.setState({
      isLogin: nextProps.isLogin,
    });
  }
  if (nextProps.isLogin) {
    this.handleClose();
  }
}

// after
static getDerivedStateFromProps(nextProps, prevState) {
  if (nextProps.isLogin !== prevState.isLogin) { // 被對比的props會被保存一份在state裏
    return {
      isLogin: nextProps.isLogin, // getDerivedStateFromProps 的返回值會自動 setState
    };
  }
  return null;
}

componentDidUpdate(prevProps, prevState) {
  if (!prevState.isLogin && this.props.isLogin) {
    this.handleClose();
  }
}

UNSAVE_componentWillMount

UNSAFE_componentWillMount() 在掛載以前被調用。它在 render() 以前調用,所以在此方法中同步調用 setState() 不會觸發額外渲染。

咱們應該避免在此方法中引入任何反作用或事件訂閱,而是選用componentDidMount()

在 React 初學者剛接觸的時候,可能有這樣一個疑問:通常都是數據請求放在componentDidMount裏面,但放在componentWillMount不是會更快獲取數據嗎?

由於理解是componentWillMount在 render 以前執行,早一點執行就早拿到請求結果;可是其實無論你請求多快,都趕不上首次 render,頁面首次渲染依舊處於沒有獲取異步數據的狀態。

還有一個緣由,componentWillMount是服務端渲染惟一會調用的生命週期函數,若是你在此方法中請求數據,那麼服務端渲染的時候,在服務端和客戶端都會分別請求兩次相同的數據,這顯然也咱們想看到的結果。

特別是有了 React Fiber,更有機會被調用屢次,故請求不該該放在componentWillMount中。

還有一個錯誤的使用是在componentWillMount中訂閱事件,並在componentWillUnmount中取消掉相應的事件訂閱。事實上只有調用componentDidMount後,React 才能保證稍後調用componentWillUnmount進行清理。並且服務端渲染時不會調用componentWillUnmount,可能致使內存泄露。

還有人會將事件監聽器(或訂閱)添加到 componentWillMount 中,但這可能致使服務器渲染(永遠不會調用 componentWillUnmount)和異步渲染(在渲染完成以前可能被中斷,致使不調用 componentWillUnmount)的內存泄漏。

對於該函數,通常狀況,若是項目有使用,則是一般把現有 componentWillMount 中的代碼遷移至 componentDidMount 便可。

UNSAFE_componentWillUpdate

當組件收到新的 props 或 state 時,會在渲染以前調用 UNSAFE_componentWillUpdate()。使用此做爲在更新發生以前執行準備更新的機會。初始渲染不會調用此方法。

注意,不能在該方法中調用 this.setState();在 componentWillUpdate 返回以前,你也不該該執行任何其餘操做(例如,dispatch Redux 的 action)觸發對 React 組件的更新。

首先跟上面兩個函數同樣,該函數也發生在 render 以前,也存在一次更新被調用屢次的可能,從這一點上看就依然不可取了。

其次,該方法常見的用法是在組件更新前,讀取當前某個 DOM 元素的狀態,並在 componentDidUpdate 中進行相應的處理。但 React 16 版本後有 suspense、異步渲染機制等等,render 過程能夠被分割成屢次完成,還能夠被暫停甚至回溯,這致使 componentWillUpdatecomponentDidUpdate 執行先後可能會間隔很長時間,這致使 DOM 元素狀態是不安全的,由於這時的值頗有可能已經失效了。並且足夠使用戶進行交互操做更改當前組件的狀態,這樣可能會致使難以追蹤的 BUG。

爲了解決這個問題,因而就有了新的生命週期函數:

getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate 在最近一次渲染輸出(提交到 DOM 節點)以前調用。它使得組件能在發生更改以前從 DOM 中捕獲一些信息(例如,滾動位置)。今生命週期的任何返回值將做爲第三個參數傳入 componentDidUpdate(prevProps, prevState, snapshot)

componentWillUpdate 不一樣,getSnapshotBeforeUpdate 會在最終的 render 以前被調用,也就是說在 getSnapshotBeforeUpdate 中讀取到的 DOM 元素狀態是能夠保證與 componentDidUpdate 中一致的。

雖然 getSnapshotBeforeUpdate 不是一個靜態方法,但咱們也應該儘可能使用它去返回一個值。這個值會隨後被傳入到 componentDidUpdate 中,而後咱們就能夠在 componentDidUpdate 中去更新組件的狀態,而不是在 getSnapshotBeforeUpdate 中直接更新組件狀態。避免了 componentWillUpdatecomponentDidUpdate 配合使用時將組件臨時的狀態數據存在組件實例上浪費內存,getSnapshotBeforeUpdate 返回的數據在 componentDidUpdate 中用完即被銷燬,效率更高。

來看官方的一個例子:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 咱們是否在 list 中添加新的 items?
    // 捕獲滾動位置以便咱們稍後調整滾動位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 若是咱們 snapshot 有值,說明咱們剛剛添加了新的 items,
    // 調整滾動位置使得這些新 items 不會將舊的 items 推出視圖。
    //(這裏的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

若是項目中有用到componentWillUpdate的話,升級方案就是將現有的 componentWillUpdate 中的回調函數遷移至 componentDidUpdate。若是觸發某些回調函數時須要用到 DOM 元素的狀態,則將對比或計算的過程遷移至 getSnapshotBeforeUpdate,而後在 componentDidUpdate 中統一觸發回調或更新狀態。

除了這些,React 16 版本的依然還有大改動,其中引人注目的就是 Fiber,以後我還會抽空寫一篇關於 React Fiber 的文章,能夠關注個人我的技術博文 Github 倉庫,以爲不錯的話歡迎 star,給我一點鼓勵繼續寫做吧~

參考:

相關文章
相關標籤/搜索