在即将到来的 react17.0 版本,react 团队对生命周期做了调整,将会移除 componentWillMountcomponentWillReceivePropscomponentWillUpdate这三个生命周期,因为这些生命周期方法容易被误解和滥用。

组件数据初始化

一般我们为了提前 setState ,防止二次渲染(第一次是空 state 渲染,第二次外部数据渲染),经常在 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 来调用

还有一个问题是,componentWillMount 在服务端渲染(nuxt.js)的时候会导致服务端和客户端各渲染一次,而 componentDidMount 只在客户端渲染一次

// After
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentDidMount() {
this._asyncRequest = loadMyAsyncData().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 来实现,因为只有在调用 componentDidMount 的时候,React 才会确保 componentWillUnmount 回调能顺利执行,防止内存泄漏  😁

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,
});
};
}

基于 props 更新 state

我们经常会在 componentWillReceiveProps  来做 props 比较,然后更新组件的 state

class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
};
componentWillReceiveProps(nextProps) {
if (this.props.currentRow !== nextProps.currentRow) {
this.setState({
isScrollingDown: nextProps.currentRow > this.props.currentRow,
});
}
}
}

从版本 16.3 开始,更新 state 以响应 props 更改的推荐方法是使用新的静态 getDerivedStateFromProps 生命周期。 (生命周期在组件创建时以及每次收到新的 props 时调用)

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;
}
}

getDerivedStateFromProps 有两个参数 nextPropsprevState,第一个是用来获取新的 props,第二个参数可以获取组件的上一个 state,有可能有个疑问,为什么不把上一个 props 也传递过来,React 团队在设计的时候考虑过这个问题,有两个原因

  • 在第一次调用  getDerivedStateFromProps(实例化后)时,prevProps参数将为 null,需要在访问 prevProps 时添加 if-not-null 检查
  • 没有将以前的props传递给这个函数,可以把之前不需要用的 props 释放掉,避免内存占用

调用外部组件的回调函数

如果我们需要在一个在内部状态发生变化时,调用外部组件的函数做一些事情,我们可能会这样做

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

但是问题是,在异步模式下使用  componentWillUpdate  都是不安全的,因为外部回调可能在组件的一次 state 更新下多次调用。相反,应该使用  componentDidUpdate  生命周期,因为它保证每次更新只调用一次

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

基于 props 改变获取服务端数据

我们一般会在 componentWillReceiveProps 的回调里面判断,然后 _loadAsyncData 获取接口数据

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 });
});
}
}

这样虽然没毛病,但是为了兼容新的 api,官方推荐的做法是在  getDerivedStateFromProps 回调里面处理传递过来的 props,然后将异步获取数据放在  componentDidUpdate

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 });
});
}
}

在更新之前读取 dom 的属性

在更新一个列表容器数据的时候,我们需要保持滚动条的位置,可以在  getSnapshotBeforeUpdate 新的生命周期里面去获取 dom 的属性,例如offsetHeightscrollHeight等属性,它可以将 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;
};
}

新版如何兼容旧的 API

可以通过  react-lifecycles-compat 可以使新的  getDerivedStateFromProps  生命周期与旧版本的 React 一起使用。