ReactV16.3,行將變動的生命周期

解釋:本文是依據React的官方博客翻譯而成(文章地點:https://reactjs.org/blog/2018…)。
主要報告了React以後的更新方向,以及對之前生命周期所湧現的題目標總結,以後的React將逐漸棄用一些生命周期和增添一些更有效更相符實際狀況的生命周期。个中也為從傳統的生命周期遷徙到新版本的React提出了一些處理要領。

一年多來,React團隊一向致力於完成異步襯着。上個月,他在JSConf冰島的演講中,丹展現了一些令人興奮的新的異步襯着能夠性。如今,我們願望與您分享我們在進修這些功用時學到的一些經驗教訓,以及一些協助您預備組件以在啟動時舉行異步襯着的要領。

我們相識到的最大題目之一是,我們的一些傳統組件生命周期會致使一些不安全的編碼實踐。他們是:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

這些生命周期要領常常被誤會和濫用;另外,我們估計他們的潛伏濫用能夠在異步襯着方面有更大的題目。因而,我們將在行將宣布的版本中為這些生命周期增加一個“UNSAFE_”前綴。 (這裏,“不安全”不是指安全性,而是示意運用這些生命周期的代碼將更有能夠在將來的React版本中存在缺點,特別是一旦啟用了異步襯着)。

[](https://reactjs.org/#gradual-…

React遵照語義版本掌握, 所以這類轉變將是漸進的。我們現在的設想是:

  • 16.3:為不安全生命周期引入別號UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProps和UNSAFE_componentWillUpdate。 (舊的生命周期稱號和新的別號都能夠在此版本中運用。)
  • 將來的16.x版本:為componentWillMount,componentWillReceiveProps和componentWillUpdate啟用棄用正告。 (舊的生命周期稱號和新的別號都能夠在此版本中運用,但舊稱號會紀錄DEV形式正告。)
  • 17.0:刪除componentWillMount,componentWillReceiveProps和componentWillUpdate。 (從如今最先,只要新的“UNSAFE_”生命周期稱號將起作用。)

請注重,假如您是React運用順序開發職員,那末您沒必要對遺留要領舉行任何操縱。行將宣布的16.3版本的主要目標是讓開源項目保護職員在任何棄用正告之前更新其庫。這些正告將在將來的16.x版本宣布之前不會啟用。

我們在Facebook上保護了凌駕50,000個React組件,我們不盤算馬上重寫它們。我們曉得遷徙須要時候。我們將採納逐漸遷徙途徑以及React社區中的所有人。

從傳統生命周期遷徙

假如您想最先運用React 16.3中引入的新組件API(或許假如您是保護職員提早更新庫),以下是一些示例,我們願望這些示例能夠協助您最先斟酌組件的變化。跟着時候的推移,我們設想在文檔中增加分外的“配方”,以展現怎樣以防止有題目標生命周期的體式格局實行罕見使命。

在最先之前,我們將扼要概述為16.3版設想的生命周期變動:

  • We are adding the following lifecycle aliases: UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps, and UNSAFE_componentWillUpdate. (Both the old lifecycle names and the new aliases will be supported.)
  • We are introducing two new lifecycles, static getDerivedStateFromProps and getSnapshotBeforeUpdate.
  • 我們正在增加以下生命周期別號

(1) UNSAFE_componentWillMount,

(2) UNSAFE_componentWillReceiveProps

(3) UNSAFE_componentWillUpdate。 (舊的生命周期稱號和新的別號都將受支撐。)

  • 我們引見了兩個新的生命周期,分別是getDerivedStateFromProps和getSnapshotBeforeUpdate。

新的生命周期: getDerivedStateFromProps

class Example extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    // ...
  }
}

新的靜態getDerivedStateFromProps生命周期在組件實例化以及吸收新props后挪用。它能夠返回一個對象來更新state,或許返回null來示意新的props不須要任何state更新。

componentDidUpdate一同,這個新的生命周期應當掩蓋傳統componentWillReceiveProps的所有效例。

新的生命周期: getSnapshotBeforeUpdate

class Example extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // ...
  }
}

新的getSnapshotBeforeUpdate生命周期在更新之前被挪用(比方,在DOM被更新之前)。此生命周期的返回值將作為第三個參數通報給componentDidUpdate。 (這個生命周期不是常常須要的,但能夠用於在恢復時期手動保留轉動位置的狀況。)

componentDidUpdate一同,這個新的生命周期將掩蓋舊版componentWillUpdate的所有效例。

You can find their type signatures in this gist.

我們看看怎樣在運用這兩種生命周期的,例子以下:

比方:

注重

為簡約起見,下面的示例是運用試驗類屬性轉換編寫的,但假如沒有它,則運用雷同的遷徙戰略。

初始化狀況:

這個例子展現了一個挪用componentWillMount中帶有setState的組件:

// Before
class ExampleComponent extends React.Component {
  state = {};

  componentWillMount() {
    this.setState({
      currentColor: this.props.defaultColor,
      palette: 'rgb',
    });
  }
}

這類範例的組件最簡樸的重構是將狀況初始化移動到組織函數或屬性初始值設定項,以下所示:

// After
class ExampleComponent extends React.Component {
  state = {
    currentColor: this.props.defaultColor,
    palette: 'rgb',
  };
}

獵取外部數據

以下是運用componentWillMount獵取外部數據的組件示例:

// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentWillMount() {
    this._asyncRequest = asyncLoadData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

上述代碼關於服務器顯現(个中不運用外部數據的地方)和行將到來的異步顯現形式(个中要求能夠被屢次啟動)是有題目標。

關於大多數用例,發起的晉級途徑是將數據提取移入componentDidMount

// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._asyncRequest = asyncLoadData().then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }
}

有一個罕見的毛病觀念以為,在componentWillMount中提取能夠防止第一個空的襯着。在實踐中,這歷來都不是真的,由於React老是在componentWillMount以後馬上實行襯着。假如數據在componentWillMount觸發的時候內不可用,則不管你在那裡提取數據,第一個襯着仍將顯現加載狀況。這就是為安在絕大多數狀況下將提取移到componentDidMount沒有顯著結果。

注重:

一些高等用例(比方,像Relay如許的庫)能夠想要嘗試運用熱切的預取異步數據。在這裡能夠找到一個如許做的
例子

從長遠來看,在React組件中獵取數據的範例體式格局能夠基於JSConf冰島推出的“牽挂”API。簡樸的數據提取處理方案以及像Apollo和Relay如許的庫都能夠在背景運用。它比上述任一處理方案的冗餘性都要小得多,但不會在16.3版本中實時完成。

當支撐服務器襯着時,現在須要同步供應數據 – componentWillMount一般用於此目標,但組織函數能夠用作替換。行將到來的牽挂API將使得異步數據在客戶端和服務器顯現中都能夠清楚地獵取。

增加時候監聽

下面是一個在裝置時監聽外部事宜調理順序的組件示例:

// Before
class ExampleComponent extends React.Component {
  componentWillMount() {
    this.setState({
      subscribedValue: this.props.dataSource.value,
    });

    // This is not safe; it can leak!
    this.props.dataSource.subscribe(
      this.handleSubscriptionChange
    );
  }

  componentWillUnmount() {
    this.props.dataSource.unsubscribe(
      this.handleSubscriptionChange
    );
  }

  handleSubscriptionChange = dataSource => {
    this.setState({
      subscribedValue: dataSource.value,
    });
  };
}

不幸的是,這會致使服務器襯着(componentWillUnmount永久不會被挪用)和異步襯着(在襯着完成之前襯着能夠被中綴,致使componentWillUnmount不被挪用)的內存走漏。

人們常常以為componentWillMountcomponentWillUnmount老是配對,但這並不能保證。只要挪用componentDidMount后,React才保證稍後挪用componentWillUnmount舉行清算。

出於這個緣由,增加事宜監聽的引薦體式格局是運用componentDidMount生命周期:

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

  componentDidMount() {
    // Event listeners are only safe to add after mount,
    // So they won't leak if mount is interrupted or errors.
    this.props.dataSource.subscribe(
      this.handleSubscriptionChange
    );

    // External values could change between render and mount,
    // In some cases it may be important to handle this case.
    if (
      this.state.subscribedValue !==
      this.props.dataSource.value
    ) {
      this.setState({
        subscribedValue: this.props.dataSource.value,
      });
    }
  }

  componentWillUnmount() {
    this.props.dataSource.unsubscribe(
      this.handleSubscriptionChange
    );
  }

  handleSubscriptionChange = dataSource => {
    this.setState({
      subscribedValue: dataSource.value,
    });
  };
}

偶然候更新監聽以相應屬性變化很主要。假如您運用的是像Redux或MobX如許的庫,庫的容器組件會為您處置懲罰。關於運用順序作者,我們建立了一個小型庫create-subscription來協助處理這個題目。我們會將它與React 16.3一同宣布。

Rather than passing a subscribable dataSource prop as we did in the example above, we could use create-subscription to pass in the subscribed value:

我們能夠運用create-subscription來通報監聽的值,而不是像上例那樣通報監聽 的dataSource prop。

import {createSubscription} from 'create-subscription';

const Subscription = createSubscription({
  getCurrentValue(sourceProp) {
    // Return the current value of the subscription (sourceProp).
    return sourceProp.value;
  },

  subscribe(sourceProp, callback) {
    function handleSubscriptionChange() {
      callback(sourceProp.value);
    }

    // Subscribe (e.g. add an event listener) to the subscription (sourceProp).
    // Call callback(newValue) whenever a subscription changes.
    sourceProp.subscribe(handleSubscriptionChange);

    // Return an unsubscribe method.
    return function unsubscribe() {
      sourceProp.unsubscribe(handleSubscriptionChange);
    };
  },
});

// Rather than passing the subscribable source to our ExampleComponent,
// We could just pass the subscribed value directly:
`<Subscription source={dataSource}>`
  {value => `<ExampleComponent subscribedValue={value} />`}
`</Subscription>`;

注重>>像Relay / Apollo如許的庫應當運用與建立定閱雷同的手藝手動治理定閱(云云地方援用的),並採納最適合其庫運用的優化體式格局。

基於props更新state

以下是運用舊版componentWillReceiveProps生命周期基於新的道具值更新狀況的組件示例:

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

  componentWillReceiveProps(nextProps) {
    if (this.props.currentRow !== nextProps.currentRow) {
      this.setState({
        isScrollingDown:
          nextProps.currentRow > this.props.currentRow,
      });
    }
  }
}

只管上面的代碼自身並沒有題目,但componentWillReceiveProps生命周期一般會被毛病地用於處理題目。因而,該要領將被棄用。

從版本16.3最先,更新state以相應props變動的引薦要領是運用新的靜態getDerivedStateFromProps生命周期。 (生命周期在組件建立時以及每次收到新道具時挪用):

// After
class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.currentRow !== prevState.lastRow) {
      return {
        isScrollingDown:
          nextProps.currentRow > prevState.lastRow,
        lastRow: nextProps.currentRow,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

You may notice in the example above that props.currentRow is mirrored in state (as state.lastRow). This enables getDerivedStateFromProps to access the previous props value in the same way as is done in componentWillReceiveProps.

你能夠會注重到在上面的例子中,props.currentRow是一個鏡像狀況(如state.lastRow)。這使得getDerivedStateFromProps能夠像在componentWillReceiveProps中一樣接見之前的props值。

您能夠想曉得為何我們不只是將先前的props作為參數通報給getDerivedStateFromProps。我們在設想API時斟酌了這個選項,但終究決議阻擋它,緣由有兩個:

  • A prevProps parameter would be null the first time getDerivedStateFromProps was called (after instantiation), requiring an if-not-null check to be added any time prevProps was accessed.
  • Not passing the previous props to this function is a step toward freeing up memory in future versions of React. (If React does not need to pass previous props to lifecycles, then it does not need to keep the previous props object in memory.)
  1. 在第一次挪用getDerivedStateFromProps(實例化后)時,prevProps參數將為null,須要在接見prevProps時增加if-not-null搜檢。
  2. 沒有將之前的props通報給這個函數,在將來版本的React中開釋內存的一個步驟。 (假如React不須要將先前的道具通報給生命周期,那末它不須要將先前的道具對象保留在內存中。)

注重:假如您正在編寫同享組件,那末
react-lifecycles-compat polyfill能夠使新的
getDerivedStateFromProps生命周期與舊版本的React一同運用。細緻相識怎樣在下面運用它。

挪用外部回調函數

下面是一個在內部狀況發作變化時挪用外部函數的組件示例:

// Before
class ExampleComponent extends React.Component {
  componentWillUpdate(nextProps, nextState) {
    if (
      this.state.someStatefulValue !==
      nextState.someStatefulValue
    ) {
      nextProps.onChange(nextState.someStatefulValue);
    }
  }
}

在異步形式下運用componentWillUpdate都是不安全的,由於外部回調能夠會屢次挪用只更新一次。相反,應當運用componentDidUpdate生命周期,由於它保證每次更新只挪用一次:

// After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (
      this.state.someStatefulValue !==
      prevState.someStatefulValue
    ) {
      this.props.onChange(this.state.someStatefulValue);
    }
  }
}

props轉變的副作用

與上述 事例相似,偶然組件在道具變動時會發作副作用。

// Before
class ExampleComponent extends React.Component {
  componentWillReceiveProps(nextProps) {
    if (this.props.isVisible !== nextProps.isVisible) {
      logVisibleChange(nextProps.isVisible);
    }
  }
}

componentWillUpdate一樣,componentWillReceiveProps能夠會屢次挪用然則只更新一次。出於這個緣由,防止在此要領中致使的副作用非常主要。相反,應當運用componentDidUpdate,由於它保證每次更新只挪用一次:

// After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (this.props.isVisible !== prevProps.isVisible) {
      logVisibleChange(this.props.isVisible);
    }
  }
}

props轉變時獵取外部數據

以下是依據propsvalues提取外部數據的組件示例:

// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.id !== this.props.id) {
      this.setState({externalData: null});
      this._loadAsyncData(nextProps.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = asyncLoadData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}

此組件的引薦晉級途徑是將數據更新移動到componentDidUpdate中。在襯着新道具之前,您還能夠運用新的getDerivedStateFromProps生命周期消滅陳腐的數據:

// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    // Store prevId in state so we can compare when props change.
    // Clear out previously-loaded data (so we don't render stale stuff).
    if (nextProps.id !== prevState.prevId) {
      return {
        externalData: null,
        prevId: nextProps.id,
      };
    }

    // No state update necessary
    return null;
  }

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.externalData === null) {
      this._loadAsyncData(this.props.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // Render loading state ...
    } else {
      // Render real UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = asyncLoadData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}

注重>假如您運用支撐作廢的HTTP庫(如
axios),那末卸載時作廢正在舉行的要求很簡樸。關於原生Promise,
您能夠運用以下所示的要領

在更新之前讀取DOM屬性

下面是一個組件的例子,它在更新之前從DOM中讀取屬性,以便在列表中堅持轉動位置:

class ScrollingList extends React.Component {
  listRef = null;
  previousScrollOffset = null;

  componentWillUpdate(nextProps, nextState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (this.props.list.length < nextProps.list.length) {
      this.previousScrollOffset =
        this.listRef.scrollHeight - this.listRef.scrollTop;
    }
  }

  componentDidUpdate(prevProps, prevState) {
    // If previousScrollOffset is set, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    if (this.previousScrollOffset !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight -
        this.previousScrollOffset;
      this.previousScrollOffset = null;
    }
  }

  render() {
    return (
      `<div>`
        {/* ...contents... */}
      `</div>`
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

在上面的例子中,componentWillUpdate被用來讀取DOM屬性。然則,關於異步襯着,“render”階段生命周期(如componentWillUpdaterender)與“commit”階段生命周期(如componentDidUpdate)之間能夠存在耽誤。假如用戶在這段時候內做了相似調解窗口大小的操縱,則從componentWillUpdate中讀取的scrollHeight值將失效。

處理此題目標要領是運用新的“commit”階段生命周期getSnapshotBeforeUpdate。在數據發作變化之前馬上挪用該要領(比方,在更新DOM之前)。它能夠將React的值作為參數通報給componentDidUpdate,在數據發作變化后馬上挪用它。

這兩個生命周期能夠像如許一同運用:

class ScrollingList extends React.Component {
  listRef = null;

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      return (
        this.listRef.scrollHeight - this.listRef.scrollTop
      );
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      `<div>`
        {/* ...contents... */}
      `</div>`
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

注重>>假如您正在編寫同享組件,那末
react-lifecycles-compat polyfill能夠使新的
getSnapshotBeforeUpdate生命周期與舊版本的React一同運用。
細緻相識怎樣運用它

別的狀況

While we tried to cover the most common use cases in this post, we recognize that we might have missed some of them. If you are using componentWillMount, componentWillUpdate, or componentWillReceiveProps in ways that aren’t covered by this blog post, and aren’t sure how to migrate off these legacy lifecycles, please file a new issue against our documentation with your code examples and as much background information as you can provide. We will update this document with new alternative patterns as they come up.

除了以上的一些罕見的例子,還能夠會有別的狀況本篇文章沒有涵蓋到,假如您以本博文未觸及的體式格局運用componentWillMountcomponentWillUpdatecomponentWillReceiveProps,而且不確定怎樣遷徙這些傳統生命周期,你能夠供應您的代碼示例和我們的文檔,而且一同提交一個新題目。我們將在更新這份文件時供應新的替換形式。

開源項目保護者

開源保護職員能夠想曉得這些變動關於同享組件意味着什麼。假如完成上述發起,那末依靠於新的靜態getDerivedStateFromProps生命周期的組件會發作什麼狀況?你是不是還必須宣布一個新的主要版本,並下降React 16.2及更高版本的兼容性?

當React 16.3宣布時,我們還將宣布一個新的npm包, react-lifecycles-compat。該npm包會添補組件,以便新的getDerivedStateFromPropsgetSnapshotBeforeUpdate生命周期也能夠與舊版本的React(0.14.9+)一同運用。

要運用這個polyfill,起首將它作為依靠項增加到您的庫中:

# Yarn
yarn add react-lifecycles-compat

# NPM
npm install react-lifecycles-compat --save

接下來,更新您的組件以運用新的生命周期(如上所述)。

末了,運用polyfill將組件向後兼容舊版本的React:

import React from 'react';
import {polyfill} from 'react-lifecycles-compat';

class ExampleComponent extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    // Your state update logic here ...
  }
}

// Polyfill your component to work with older versions of React:
polyfill(ExampleComponent);

export default ExampleComponent;

文章泉源

    原文作者:_小生_
    原文地址: https://segmentfault.com/a/1190000014456811
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞