一年多來,React團隊一直致力於實現非同步渲染。上個月,他在JSConf冰島的演講中,丹揭示了一些令人興奮的新的非同步渲染可能性。現在,我們希望與您分享我們在學習這些功能時學到的一些經驗教訓,以及一些幫助您準備元件以在啟動時進行非同步渲染的方法。
我們瞭解到的最大問題之一是,我們的一些傳統元件生命週期會導致一些不安全的編碼實踐。他們是:
componentWillMount
componentWillReceiveProps
componentWillUpdate
這些生命週期方法經常被誤解和濫用;此外,我們預計他們的潛在濫用可能在非同步渲染方面有更大的問題。因此,我們將在即將釋出的版本中為這些生命週期新增一個“UNSAFE_”字首。 (這裡,“不安全”不是指安全性,而是表示使用這些生命週期的程式碼將更有可能在未來的React版本中存在缺陷,特別是一旦啟用了非同步渲染)。
逐步遷移路徑
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
, andUNSAFE_componentWillUpdate
. (Both the old lifecycle names and the new aliases will be supported.) - We are introducing two new lifecycles, static
getDerivedStateFromProps
andgetSnapshotBeforeUpdate
. - 我們正在新增以下生命週期別名:
(1) UNSAFE_componentWillMount,
(2) UNSAFE_componentWillReceiveProps
(3) UNSAFE_componentWillUpdate。 (舊的生命週期名稱和新的別名都將受支援。)
- 我們介紹了兩個新的生命週期,分別是getDerivedStateFromProps和getSnapshotBeforeUpdate。
新的生命週期: getDerivedStateFromProps
1 2 3 4 5 |
class Example extends React.Component { static getDerivedStateFromProps(nextProps, prevState) { // ... } } |
新的靜態getDerivedStateFromProps
生命週期在元件例項化以及接收新props
後呼叫。它可以返回一個物件來更新state
,或者返回null來表示新的props
不需要任何state
更新。
與componentDidUpdate
一起,這個新的生命週期應該覆蓋傳統componentWillReceiveProps
的所有用例。
新的生命週期: getSnapshotBeforeUpdate
1 2 3 4 5 |
class Example extends React.Component { getSnapshotBeforeUpdate(prevProps, prevState) { // ... } } |
新的getSnapshotBeforeUpdate
生命週期在更新之前被呼叫(例如,在DOM被更新之前)。此生命週期的返回值將作為第三個引數傳遞給componentDidUpdate
。 (這個生命週期不是經常需要的,但可以用於在恢復期間手動儲存滾動位置的情況。)
與componentDidUpdate
一起,這個新的生命週期將覆蓋舊版componentWillUpdate
的所有用例。
You can find their type signatures in this gist.
我們看看如何在使用這兩種生命週期的,例子如下:
例如:
- Initializing state(初始化狀態)
- Fetching external data(獲取外部資料)
- Adding event listeners (or subscriptions)(新增事件監聽)
- Updating
state
based on props(基於props
更新state
) - Invoking external callbacks(呼叫外部的
callbacks
) - Side effects on props change
- Fetching external data when props change(
props
改變時獲取外部資料) - Reading DOM properties before an update(在更新之前讀取DOM屬性)
注意
為簡潔起見,下面的示例是使用實驗類屬性轉換編寫的,但如果沒有它,則應用相同的遷移策略。
初始化狀態:
這個例子展示了一個呼叫componentWillMount
中帶有setState
的元件:
1 2 3 4 5 6 7 8 9 10 11 |
// Before class ExampleComponent extends React.Component { state = {}; componentWillMount() { this.setState({ currentColor: this.props.defaultColor, palette: 'rgb', }); } } |
這種型別的元件最簡單的重構是將狀態初始化移動到建構函式或屬性初始值設定項,如下所示:
1 2 3 4 5 6 7 |
// After class ExampleComponent extends React.Component { state = { currentColor: this.props.defaultColor, palette: 'rgb', }; } |
獲取外部資料
以下是使用componentWillMount
獲取外部資料的元件示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 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
將使得非同步資料在客戶端和伺服器呈現中都可以清晰地獲取。
新增時間監聽
下面是一個在安裝時監聽外部事件排程程式的元件示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 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生命週期:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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
生命週期基於新的道具值更新狀態的元件示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 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生命週期。 (生命週期在元件建立時以及每次收到新道具時呼叫):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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時考慮了這個選項,但最終決定反對它,原因有兩個:
- A
prevProps
parameter would be null the first timegetDerivedStateFromProps
was called (after instantiation), requiring an if-not-null check to be added any timeprevProps
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.) - 在第一次呼叫
getDerivedStateFromProps
(例項化後)時,prevProps
引數將為null,需要在訪問prevProps時新增if-not-null檢查。 - 沒有將以前的
props
傳遞給這個函式,在未來版本的React中釋放記憶體的一個步驟。 (如果React不需要將先前的道具傳遞給生命週期,那麼它不需要將先前的道具物件保留在記憶體中。)
Note
如果您正在編寫共享元件,那麼
react-lifecycles-compat polyfill
可以使新的getDerivedStateFromProps
生命週期與舊版本的React一起使用。詳細瞭解如何在下面使用它。
呼叫外部回撥函式
下面是一個在內部狀態發生變化時呼叫外部函式的元件示例:
1 2 3 4 5 6 7 8 9 10 11 |
// Before class ExampleComponent extends React.Component { componentWillUpdate(nextProps, nextState) { if ( this.state.someStatefulValue !== nextState.someStatefulValue ) { nextProps.onChange(nextState.someStatefulValue); } } } |
在非同步模式下使用componentWillUpdate
都是不安全的,因為外部回撥可能會多次呼叫只更新一次。相反,應該使用componentDidUpdate
生命週期,因為它保證每次更新只呼叫一次:
1 2 3 4 5 6 7 8 9 10 11 |
// After class ExampleComponent extends React.Component { componentDidUpdate(prevProps, prevState) { if ( this.state.someStatefulValue !== prevState.someStatefulValue ) { this.props.onChange(this.state.someStatefulValue); } } } |
props改變的副作用
與上述 事例類似,有時元件在道具更改時會產生副作用。
1 2 3 4 5 6 7 8 |
// Before class ExampleComponent extends React.Component { componentWillReceiveProps(nextProps) { if (this.props.isVisible !== nextProps.isVisible) { logVisibleChange(nextProps.isVisible); } } } |
與componentWillUpdate
一樣,componentWillReceiveProps
可能會多次呼叫但是隻更新一次。出於這個原因,避免在此方法中導致的副作用非常重要。相反,應該使用componentDidUpdate
,因為它保證每次更新只呼叫一次:
1 2 3 4 5 6 7 8 |
// After class ExampleComponent extends React.Component { componentDidUpdate(prevProps, prevState) { if (this.props.isVisible !== prevProps.isVisible) { logVisibleChange(this.props.isVisible); } } } |
props改變時獲取外部資料
以下是根據props
values提取外部資料的元件示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// 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
生命週期清除陳舊的資料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// 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中讀取屬性,以便在列表中保持滾動位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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
,在資料發生變化後立即呼叫它。
這兩個生命週期可以像這樣一起使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
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,首先將它作為依賴項新增到您的庫中:
1 2 3 4 5 |
# Yarn yarn add react-lifecycles-compat # NPM npm install react-lifecycles-compat --save |
接下來,更新您的元件以使用新的生命週期(如上所述)。
最後,使用polyfill將元件向後相容舊版本的React:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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; |