React 官方正式发布了 v16.3 版本。在这次的更新中,除了前段时间被热烈讨论的新 Context API
之外,新引入的两个生命周期函数 getDerivedStateFromProps
,getSnapshotBeforeUpdate
以及在未来 v17.0 版本中即将被移除的三个生命周期函数 componentWillMount
,componentWillReceiveProps
,componentWillUpdate
.
在 React 应用中,许多开发者为了避免第一次渲染时页面因为没有获取到异步数据导致的白屏,而将数据请求部分的代码放在了 componentWillMount
中,希望可以避免白屏并提早异步请求的发送时间。但事实上在 componentWillMount
执行后,第一次渲染就已经开始了,所以如果在 componentWillMount
执行时还没有获取到异步数据的话,页面首次渲染时也仍然会处于没有异步数据的状态。换句话说,组件在首次渲染时总是会处于没有异步数据的状态,所以不论在哪里发送数据请求,都无法直接解决这一问题。而关于提早发送数据请求,官方也鼓励将数据请求部分的代码放在组件的 constructor
中,而不是 componentWillMount
。
另一个常见的用例是在 componentWillMount
中订阅事件,并在 componentWillUnmount
中取消掉相应的事件订阅。但事实上 React 并不能够保证在 componentWillMount
被调用后,同一组件的 componentWillUnmount
也一定会被调用。一个当前版本的例子如服务端渲染时,componentWillUnmount
是不会在服务端被调用的,所以在 componentWillMount
中订阅事件就会直接导致服务端的内存泄漏。另一方面,在未来 React 开启异步渲染模式后,在 componentWillMount
被调用之后,组件的渲染也很有可能会被其他的事务所打断,导致 componentWillUnmount
不会被调用。而 componentDidMount
就不存在这个问题,在 componentDidMount
被调用后,componentWillUnmount
一定会随后被调用到,并根据具体代码清除掉组件中存在的事件订阅。
在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWillReceiveProps
中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但很多情况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。
在新版本中,React 官方提供了一个更为简洁的生命周期函数:
static getDerivedStateFromProps(nextProps, prevState)
一个简单的例子如下:
// before
componentWillReceiveProps(nextProps) {
if (nextProps.translateX !== this.props.translateX) {
this.setState({
translateX: nextProps.translateX,
});
}
}
// after
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.translateX !== prevState.translateX) {
return {
translateX: nextProps.translateX,
};
}
return null;
}
乍看下来这二者好像并没有什么本质上的区别,但这却是笔者认为非常能够体现 React 团队对于软件工程深刻理解的一个改动,即 React 团队试图通过框架级别的 API 来约束或者说帮助开发者写出可维护性更佳的 JavaScript 代码。为了解释这点,我们再来看一段代码:
// before
componentWillReceiveProps(nextProps) {
if (nextProps.isLogin !== this.props.isLogin) {
this.setState({
isLogin: nextProps.isLogin,
});
}
if (nextProps.isLogin) {
this.handleClose();
}
}
// after
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.isLogin !== prevState.isLogin) {
return {
isLogin: nextProps.isLogin,
};
}
return null;
}
componentDidUpdate(prevProps, prevState) {
if (!prevState.isLogin && this.props.isLogin) {
this.handleClose();
}
}
通常来讲,在 componentWillReceiveProps
中,我们一般会做以下两件事,一是根据 props
来更新 state
,二是触发一些回调,如动画或页面跳转等。在老版本的 React 中,这两件事我们都需要在 componentWillReceiveProps
中去做。而在新版本中,官方将更新 state
与触发回调重新分配到了 getDerivedStateFromProps
与 componentDidUpdate
中,使得组件整体的更新逻辑更为清晰。而且在 getDerivedStateFromProps
中还禁止了组件去访问 this.props
,强制让开发者去比较 nextProps
与 prevState
中的值,以确保当开发者用到 getDerivedStateFromProps
这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。
与 componentWillReceiveProps
类似,许多开发者也会在 componentWillUpdate
中根据 props
的变化去触发一些回调。但不论是 componentWillReceiveProps
还是 componentWillUpdate
,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount
类似,componentDidUpdate
也不存在这样的问题,一次更新中 componentDidUpdate
只会被调用一次,所以将原先写在 componentWillUpdate
中的回调迁移至 componentDidUpdate
就可以解决这个问题。
另一个常见的 componentWillUpdate
的用例是在组件更新前,读取当前某个 DOM 元素的状态,并在 componentDidUpdate
中进行相应的处理。但在 React 开启异步渲染模式后,render
阶段和 commit
阶段之间并不是无缝衔接的,也就是说在 render
阶段读取到的 DOM
元素状态并不总是和 commit
阶段相同,这就导致在
componentDidUpdate
中使用 componentWillUpdate
中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
为了解决上面提到的这个问题,React 提供了一个新的生命周期函数:
getSnapshotBeforeUpdate(prevProps, prevState)
与 componentWillUpdate
不同,getSnapshotBeforeUpdate
会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate
中读取到的 DOM 元素状态是可以保证与 componentDidUpdate
中一致的。虽然 getSnapshotBeforeUpdate
不是一个静态方法,但我们也应该尽量使用它去返回一个值。这个值会随后被传入到 componentDidUpdate
中,然后我们就可以在 componentDidUpdate
中去更新组件的状态,而不是在 getSnapshotBeforeUpdate
中直接更新组件状态。
官方提供的一个例子如下:
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 ref={this.setListRef}>
{/* ...contents... */}
div>
);
}
setListRef = ref => {
this.listRef = ref;
};
}
最后,让我们从整体的角度再来看一下 React 这次生命周期函数调整前后的异同:
Before
after
在第一张图中被红框圈起来的三个生命周期函数就是在新版本中即将被移除的。通过上述的两张图,我们可以清楚地看到将要被移除的三个生命周期函数都是在 render
之前会被调用到的。而根据原来的设计,在这三个生命周期函数中都可以去做一些诸如发送请求,setState
等包含副作用的事情。在老版本的 React 中,这样做也许只会带来一些性能上的损耗,但在 React 开启异步渲染模式之后,就无法再接受这样的副作用产生了。举一个 Git 的例子就是在开发者 commit 了 10 个文件更新后,又对当前或其他的文件做了另外的更新,但在 push 时却仍然只 push 了刚才 commit 的 10 个文件更新。这样就会导致提交记录与实际更新不符,如果想要避免这个问题,就需要保证每一次的文件更新都要经过 commit 阶段,再被提交到远端,而这也就是 React 在开启异步渲染模式之后要做到的。
当然,以上的这些生命周期函数的改动,一直要到 React 17.0 中才会实装,这给广大的 React 开发者们预留了充足的时间去适应这次改动。但如果你是 React 开源项目(尤其是组件库)的维护者的话,不妨花点时间去详细了解一下这次生命周期函数的改动。因为这不仅仅可以帮助你将开源项目更好地升级到 React 的最新版本,更重要的是可以帮助你提前理解即将到来的异步渲染模式。
同时,笔者也相信在 React 正式开启异步渲染模式之后,许多常用组件的性能将很有可能迎来一次整体的提升。进一步来说,配合异步渲染,许多现在的复杂组件都可以被处理得更加优雅,在代码层面得到更精细粒度上的控制,并最终为用户带来更加直观的使用体验。
转载供之后复习使用:
作者:诚身
链接:https://juejin.im/post/5ae6cd96f265da0b9c106931
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。