講講今後 React 非同步渲染帶來的生命週期變化

Enix發表於2018-03-31

如果說你是一位經驗豐富的 React 工程師,看到這邊文章講的是 React 的生命週期,已經看過無數篇關於 React 生命週期的文章的你,可能有關閉頁面的想法。

請不要急著退出,我想講一些不一樣的、來自未來的。

2018.5.24 更新:

今天 React 16.4 釋出了, getDerivedStateFromProps 的觸發時機有所改動——原本在父元件重新渲染它時才會觸發 getDerivedStateFromProps ,現改為只要渲染就會觸發,包括自身 setState 。這一變化進一步地區分了該方法與 componentWillReceiveProps ,更適合非同步渲染。

大家都知道,現在關於 React 生命週期的解析、教程、深入解讀等文章早已數不勝數。那麼為何我又要來重描一遍?因為 React 的生命週期即將改變 !React 團隊目前致力於打造非同步渲染的機制,以進一步提高 React 的渲染效能,在這一過程中,他們大刀闊斧地對生命週期做出了若干改革,以更好地適應非同步渲染的需要。

簡單說來,刪除了 3 個生命週期方法,增加了另外 2 個:

+: 新增  -: 刪除  ?: 有變化

- componentWillMount
  render
  componentDidMount
- componentWillReceiveProps
+ static getDerivedStateFromProps
  shouldComponentUpdate
- componentWillUpdate
+ getSnapshotBeforeUpdate
? componentDidUpdate
  componentWillUnmount
複製程式碼
class Example extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    // 這一生命週期方法是靜態的,它在元件例項化或接收到新的 props 時被觸發
    // 若它的返回值是物件,則將被用於更新 state ;若是 null ,則不觸發 state 的更新

    // 配合 `componentDidUpdate` 使用,這一方法可以取代 `componentWillReceiveProps`
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 該方法在實際改動(比如 DOM 更新)發生前的“瞬間”被呼叫,返回值將作為 `componentDidUpdate` 的第三個引數

    // 配合 `componentDidUpdate` 使用,這一方法可以取代 `componentWillUpdate`
  }

  componentDidUpdate(props, state, snaptshot) {
      // 新增的引數 snapshot 即是之前呼叫 getSnapshotBeforeUpdate 的返回值
  }
}
複製程式碼

具體講解

-componentWillMount

移除這一生命週期方法原因如下:

  1. 未來的非同步渲染機制中,單個元件例項也可能多次呼叫該方法
  2. 它可以被分散到 constructorcomponentDidMount

舉幾個例子:

事件繫結、非同步請求

部分經驗不足的開發者可能誤把 事件繫結 和 非同步獲取資料 的程式碼置於 componentWillMount 中,導致

a. 服務端渲染會觸發這一生命週期方法,但因往往忽略非同步獲取的資料而白白請求 或 因服務端不觸發 componentWillUnmount 而無法取消事件監聽,導致記憶體洩漏
b. 結合 1. ,多次呼叫會導致發出重複的請求 或 進行重複監聽,後者亦會導致記憶體洩漏

最佳實踐:

class ExampleComponent extends React.Component {
  state = {
    subscribedValue: this.props.dataSource.value,
    externalData: null,
  };

  componentDidMount() {
    // 這裡只觸發一次,可以安全地進行 非同步請求、事件繫結
    this.asyncRequest = asyncLoadData().then(
      externalData => {
        this.asyncRequest = null;
        this.setState({externalData});
      }
    );

    this.props.dataSource.subscribe(this.handleSubscriptionChange);
  }

  componentWillUnmount() {
    if (this.asyncRequest) this.asyncRequest.cancel();
    // 事件繫結在客戶端才進行,且只進行一次,在這裡可以安全地解綁
    this.props.dataSource.unsubscribe(this.handleSubscriptionChange);
  }

  handleSubscriptionChange = dataSource => {
    this.setState({ subscribedValue: dataSource.value });
  };
}
複製程式碼

-componentWillReceiveProps & +getDerivedStateFromProps

來看一段常見的 componentWillReceiveProps 的用法:

class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
  };

  componentWillReceiveProps(nextProps) {
    if (this.props.currentRow !== nextProps.currentRow) {
      // 檢測到變化後更新狀態、並請求資料
      this.setState({
        isScrollingDown: nextProps.currentRow > this.props.currentRow,
      });
      this.loadAsyncData()
    }
  }

  loadAsyncData() {/* ... */}
}
複製程式碼

這段程式碼其實沒有什麼大問題,但存在更好的寫法。當使用 getDerivedStateFromProps ,可以這樣寫:

class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    // 不再提供 prevProps 的獲取方式
    if (nextProps.currentRow !== prevState.lastRow) {
      return {
        isScrollingDown: nextProps.currentRow > prevState.lastRow,
        lastRow: nextProps.currentRow,
      };
    }

    // 預設不改動 state
    return null;
  }
  
  componentDidUpdate() {
    // 僅在更新觸發後請求資料
    this.loadAsyncData()
  }

  loadAsyncData() {/* ... */}
}
複製程式碼

可以看到在這種情況下,開發者們將更“自發地”採取在 componentDidUpdate 編寫觸發非同步請求的程式碼(因為別無選擇:P),避免了一個問題——外部元件多次頻繁更新傳入多次不同的 props,而該元件將這些更新 batch 後僅僅觸發單次自己的更新,如此一來前者的寫法會導致不必要的非同步請求,後者更節省資源。

-componentWillUpdate & +getSnapshotBeforeUpdate

componentWillUpdate 的常見用法是,在更新前記錄 DOM 狀態,結合更新後 componentDidUpdate 再次獲取的 DOM 狀態進行必要的處理。非同步渲染的到來,使得 componentWillUpdate 的觸發時機(它在非同步渲染被取締,但此處我們假想它仍然存在)與 componentDidUpdate 的觸發時機間隔較大,因為非同步渲染隨時可能暫緩這一元件的更新。這樣一來,之前的做法將變得不夠穩定,因為這間隔久到 DOM 可能因為使用者行為發生了變化。

為此,React 提供了 getSnapshotBeforeUpdate 。它的觸發時機是 React 進行修改前(通常是更新 DOM)的“瞬間” ,這樣一來在此獲取到的 DOM 資訊甚至比 componentWillUpdate 更加可靠。此外,它的返回值會作為第三個引數傳入 componentDidUpdate ,這樣做的好處很明顯——開發者可以不必將為了協調渲染前後狀態之用而產生的資料儲存在元件例項上,用完即可銷燬。

簡單總結一下

React 自從邁上 16 的版本號,就像坐上了火箭,效能與 API 的進化令人矚目。在不久的將來,非同步渲染 將正式登場,帶來渲染效能又一輪突破。本文所討論的生命週期變化中,可以看到 React 團隊為這一變化所做鋪墊的良苦用心——舊 API 的移除,警醒或潛移默化地使開發者們遵循更加優秀的開發方式。從 v16.3 開始, 舊的 API 將逐漸被替代,而等到了 v17 將徹底廢棄,看到這裡的各位可以開始考慮給自己的專案升升級啦!

參考連結: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html

相關文章