注释:本文是根据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;