一个月前,React 官方正式发布了 v16.3 版本。在此次的更新中,除了前段时间被热烈讨论的新 Context API 以外,新引入的两个生命周期函数 getDerivedStateFromProps
,getSnapshotBeforeUpdate
以及在将来 v17.0 版本中即将被移除的三个生命周期函数 componentWillMount
,componentWillReceiveProps
,componentWillUpdate
也很是值得咱们花点时间去探究一下其背后的缘由以及在具体项目中的升级方案。javascript
在 React 应用中,许多开发者为了不第一次渲染时页面由于没有获取到异步数据致使的白屏,而将数据请求部分的代码放在了 componentWillMount
中,但愿能够避免白屏并提前异步请求的发送时间。但事实上在 componentWillMount
执行后,第一次渲染就已经开始了,因此若是在 componentWillMount
执行时尚未获取到异步数据的话,页面首次渲染时也仍然会处于没有异步数据的状态。换句话说,组件在首次渲染时老是会处于没有异步数据的状态,因此不论在哪里发送数据请求,都没法直接解决这一问题。而关于提前发送数据请求,官方也鼓励将数据请求部分的代码放在组件的 constructor
中,而不是 componentWillMount
。java
另外一个常见的 componentWillMount
的用例是在服务端渲染时获取数据,由于在服务端渲染时 componentDidMount
是不会被调用的。针对这个问题,笔者这里提供两种解法。第一个简单的解法是将全部的数据请求都放在 componentDidMount
中,即只在客户端请求异步数据。这样作能够避免在服务端和客户端分别请求两次相同的数据(componentWillMount
在客户端渲染时一样会被调用到),但很明显的缺点就是没法在服务端渲染时获取到页面渲染所需的全部数据,因此若是咱们须要保证服务端返回的 HTML 就是用户最终看到的 HTML 的话,咱们能够将每一个页面的数据获取逻辑单独抽离出来,而后一一对应到相应的页面,在服务端根据当前页面的路由找到相应的数据请求,利用链式的 Promise 在渲染最终的页面前就将数据塞入 redux store 或其余数据管理工具中,这样服务端返回的 HTML 就是包含异步数据的结果了。git
另外一个常见的用例是在 componentWillMount
中订阅事件,并在 componentWillUnmount
中取消掉相应的事件订阅。但事实上 React 并不可以保证在 componentWillMount
被调用后,同一组件的 componentWillUnmount
也必定会被调用。一个当前版本的例子如服务端渲染时,componentWillUnmount
是不会在服务端被调用的,因此在 componentWillMount
中订阅事件就会直接致使服务端的内存泄漏。另外一方面,在将来 React 开启异步渲染模式后,在 componentWillMount
被调用以后,组件的渲染也颇有可能会被其余的事务所打断,致使 componentWillUnmount
不会被调用。而 componentDidMount
就不存在这个问题,在 componentDidMount
被调用后,componentWillUnmount
必定会随后被调用到,并根据具体代码清除掉组件中存在的事件订阅。github
将现有 componentWillMount
中的代码迁移至 componentDidMount
便可。redux
在老版本的 React 中,若是组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是须要在 componentWillReceiveProps
中判断先后两个 props 是否相同,若是不一样再将新的 props 更新到相应的 state 上去。这样作一来会破坏 state 数据的单一数据源,致使组件状态变得不可预测,另外一方面也会增长组件的重绘次数。相似的业务需求也有不少,如一个能够横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的状态,但不少状况下,业务需求会要求从外部跳转至列表时,根据传入的某个值,直接定位到某个 Tab。安全
在新版本中,React 官方提供了一个更为简洁的生命周期函数:markdown
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
中的代码根据更新 state 或回调,分别在 getDerivedStateFromProps
及 componentDidUpdate
中进行相应的重写便可,注意新老生命周期函数中 prevProps
,this.props
,nextProps
,prevState
,this.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; }; } 复制代码
将现有的 componentWillUpdate
中的回调函数迁移至 componentDidUpdate
。若是触发某些回调函数时须要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate
,而后在 componentDidUpdate
中统一触发回调或更新状态。
最后,让咱们从总体的角度再来看一下 React 此次生命周期函数调整先后的异同:
在第一张图中被红框圈起来的三个生命周期函数就是在新版本中即将被移除的。经过上述的两张图,咱们能够清楚地看到将要被移除的三个生命周期函数都是在 render 以前会被调用到的。而根据原来的设计,在这三个生命周期函数中均可以去作一些诸如发送请求,setState 等包含反作用的事情。在老版本的 React 中,这样作也许只会带来一些性能上的损耗,但在 React 开启异步渲染模式以后,就没法再接受这样的反作用产生了。举一个 Git 的例子就是在开发者 commit 了 10 个文件更新后,又对当前或其余的文件作了另外的更新,但在 push 时却仍然只 push 了刚才 commit 的 10 个文件更新。这样就会致使提交记录与实际更新不符,若是想要避免这个问题,就须要保证每一次的文件更新都要通过 commit 阶段,再被提交到远端,而这也就是 React 在开启异步渲染模式以后要作到的。
另外一方面,为了验证我的的理解及测试新版本的稳定性,笔者已经将我的负责的几个项目所有都升级到了 React 16.3 并根据上述提到的升级方案替换了全部即将被移除的生命周期函数。目前,全部项目在生产环境中都运行良好,没有收到任何不良的用户反馈。
固然,以上的这些生命周期函数的改动,一直要到 React 17.0 中才会实装,这给广大的 React 开发者们预留了充足的时间去适应此次改动。但若是你是 React 开源项目(尤为是组件库)的维护者的话,不妨花点时间去详细了解一下此次生命周期函数的改动。由于这不只仅能够帮助你将开源项目更好地升级到 React 的最新版本,更重要的是能够帮助你提早理解即将到来的异步渲染模式。
同时,笔者也相信在 React 正式开启异步渲染模式以后,许多经常使用组件的性能将颇有可能迎来一次总体的提高。进一步来讲,配合异步渲染,许多如今的复杂组件均可以被处理得更加优雅,在代码层面获得更精细粒度上的控制,并最终为用户带来更加直观的使用体验。
我的 Blog 地址:AlanWei/blog