註釋:本文是根據React的官方博客翻譯而成(文章地址:https://reactjs.org/blog/2018...)。
主要講述了React以後的更新方向,以及對以前生命週期所出現的問題的總結,以後的React將逐步棄用一些生命週期和增長一些更實用更符合實際狀況的生命週期。其中也爲從傳統的生命週期遷移到新版本的React提出了一些解決方法。html
一年多來,React團隊一直致力於實現異步渲染。上個月,他在JSConf冰島的演講中,丹揭示了一些使人興奮的新的異步渲染可能性。如今,咱們但願與您分享咱們在學習這些功能時學到的一些經驗教訓,以及一些幫助您準備組件以在啓動時進行異步渲染的方法。react
咱們瞭解到的最大問題之一是,咱們的一些傳統組件生命週期會致使一些不安全的編碼實踐。他們是:ios
componentWillMount
componentWillReceiveProps
componentWillUpdate
這些生命週期方法常常被誤解和濫用;此外,咱們預計他們的潛在濫用可能在異步渲染方面有更大的問題。所以,咱們將在即將發佈的版本中爲這些生命週期添加一個「UNSAFE_」前綴。 (這裏,「不安全」不是指安全性,而是表示使用這些生命週期的代碼將更有可能在將來的React版本中存在缺陷,特別是一旦啓用了異步渲染)。git
React遵循語義版本控制, 因此這種改變將是漸進的。咱們目前的計劃是:github
請注意,若是您是React應用程序開發人員,那麼您沒必要對遺留方法進行任何操做。即將發佈的16.3版本的主要目的是讓開源項目維護人員在任何棄用警告以前更新其庫。這些警告將在將來的16.x版本發佈以前不會啓用。npm
咱們在Facebook上維護了超過50,000個React組件,咱們不打算當即重寫它們。咱們知道遷移須要時間。咱們將採用逐步遷移路徑以及React社區中的全部人。axios
若是您想開始使用React 16.3中引入的新組件API(或者若是您是維護人員提早更新庫),如下是一些示例,咱們但願這些示例能夠幫助您開始考慮組件的變化。隨着時間的推移,咱們計劃在文檔中添加額外的「配方」,以展現如何以免有問題的生命週期的方式執行常見任務。安全
在開始以前,咱們將簡要概述爲16.3版計劃的生命週期更改:服務器
UNSAFE_componentWillMount
, UNSAFE_componentWillReceiveProps
, and UNSAFE_componentWillUpdate
. (Both the old lifecycle names and the new aliases will be supported.)getDerivedStateFromProps
and getSnapshotBeforeUpdate
.(1) UNSAFE_componentWillMount,dom
(2) UNSAFE_componentWillReceiveProps
(3) UNSAFE_componentWillUpdate。 (舊的生命週期名稱和新的別名都將受支持。)
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.
咱們看看如何在使用這兩種生命週期的,例子以下:
state
based on props(基於props
更新state
)callbacks
)props
改變時獲取外部數據)注意爲簡潔起見,下面的示例是使用實驗類屬性轉換編寫的,但若是沒有它,則應用相同的遷移策略。
這個例子展現了一個調用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
不被調用)的內存泄漏。
人們常常認爲componentWillMount
和componentWillUnmount
老是配對,但這並不能保證。只有調用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)。這使得getDerivedStateFromProp
s能夠像在componentWillReceiveProp
s中同樣訪問之前的props值。
您可能想知道爲何咱們不僅是將先前的props
做爲參數傳遞給getDerivedStateFromProps
。咱們在設計API時考慮了這個選項,但最終決定反對它,緣由有兩個:
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.props
object in memory.)getDerivedStateFromProps
(實例化後)時,prevProps
參數將爲null,須要在訪問prevProps時添加if-not-null檢查。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); } } }
與上述 事例相似,有時組件在道具更改時會產生反作用。
// 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
values提取外部數據的組件示例:
// 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中讀取屬性,以便在列表中保持滾動位置:
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」階段生命週期(如componentWillUpdate
和render
)與「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.
除了以上的一些常見的例子,還可能會有別的狀況本篇文章沒有涵蓋到,若是您以本博文未涉及的方式使用componentWillMount
,componentWillUpdate
或componentWillReceiveProps
,而且不肯定如何遷移這些傳統生命週期,你能夠提供您的代碼示例和咱們的文檔,而且一塊兒提交一個新問題。咱們將在更新這份文件時提供新的替代模式。
開源維護人員可能想知道這些更改對於共享組件意味着什麼。若是實現上述建議,那麼依賴於新的靜態getDerivedStateFromProps
生命週期的組件會發生什麼狀況?你是否還必須發佈一個新的主要版本,並下降React 16.2及更高版本的兼容性?
當React 16.3發佈時,咱們還將發佈一個新的npm包, react-lifecycles-compat
。該npm包會填充組件,以便新的getDerivedStateFromProps
和getSnapshotBeforeUpdate
生命週期也能夠與舊版本的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;