React新版本生命周期及替换方案
React 16.3版本引入了两个新的生命周期函数,getDerivedStateFromProps,getSnapshotBeforeUpdate 。还有三个componentWillMount,componentWillReceiveProps,componentWillUpdate被标记为不安全的生命周期,将在17.0版本被移除。公司的项目逐渐要将不安全的生命周期移除,为以后版本升级React 17.0作准备。因此我们需要来对比一下新旧版本的生命周期,从而得出生命周期的替换方案。
在老版本的生命周期图谱中,被红框圈起来的三个生命周期函数就是在新版本中即将被移除的。
下图中为React 16.4版本的生命周期图谱。我们可以看到,React的生命周期仍分为三种类型。
- 挂载时,挂载指的是组件实例被创建并插入DOM中。创建时有以下几个阶段,第一个为constructor,它是一个组件的构造函数,一个组件在更新到界面之前需要被创造出来。用于初始化内部状态,它是唯一直接修改state 的地方。第二个方法为getDerivedStateFromProps,它用于从外部的属性来初始化内部的状态,返回的状态可以更新到当前的状态上。第三个方法是render,是用来描述UI的dom结构。创建过程完成后会调用didmount方法,这时候所有的UI都渲染完成了,我们可以安全地操作dom节点以及调用接口来获取外部的资源。这个方法在整个生命周期中只执行一次。
- 更新时,它由组件的props或state发生变化时触发。更新时有以下几个阶段,第一个方法为getDerivedStateFromProps,第二个方法为shouldComponentUpdate, 它可以告诉组件我们是否需要render,可以用来性能优化。因为有时候我们在props变化时,界面UI并不需要变化,在方法中返回false告知不需要更新。返回true时react才会继续触发接下来的render。这个方法一般不需要自己去实现,react提供了purecomponent帮助我们判断props、state是否在前后有变化,如果没有变化它可以自己阻止react更新。pre-commit阶段调用getSnapshotBeforeUpdate,这个也是react 16.3新引入的方法。最后会有componentDidUpdate方法,这个方法每次UI发生更新时都会调用,react组件在外部属性或者内部状态变化时都会重新渲染,它始终会整体刷新,可以通过这个方法捕获每一次更新,从而判断是否需要其他操作。举一个我们项目中的栗子,审批的详情页,id通过url参数传入,用户切换到另一个审批,这个id发生变化,在方法中可以获取新的id对应的审批内容显示到界面上。
- 卸载时,组件从DOM中移除,通过willUnmount进行资源释放。
新增的生命周期为 getDerivedStateFromProps和getSnapshotBeforeUpdate。
getDerivedStateFromProps
它是react 16.3新引入的api,给我们提供了一个最佳实践——如何通过属性来初始化内部状态。它的使用场景是当state需要从props初始化来使用。这个方法的名字很长,据说是开发者们不推荐大家使用,因为如果state需要从props获得,一般都可以从props计算动态得到,不需要单独存储这个状态。因为如果一旦要单独存储,这意味着我们要始终维护两者的一致性,会增加很多的复杂度,容易出现bug。它每次更新时都会调用,这个api是用来取代componentwillreceiveprops方法。它的应用场景为我们的表单控件需要获取一个默认值,因为表单除了需要用户输入值之外,开始可能会给我们一个默认值。这个默认值一旦修改之后便没有有了。因此开始的state来源于外部的初始值,但当用户输入后state便来源于用户的输入。几乎在其他场景下我们不需要使用到这个生命周期方法。getSnapshotBeforeUpdate
页面render之前会调用,它的典型场景为获取render之前的dom状态。此生命周期的任何返回值将作为参数传递给componentDidUpdate(prevProps, prevState, snapshot)。getSnapshotBeforeUpdate 的使用场景一般是获取组件更新之前的滚动条位置。
接下来我结合项目来说明一下生命周期的替换方案。
componentWillUpdate的替换
当组件收到新的props或state时,会在render之前调用 componentWillUpdate()。
componentWillUpdate(nextProps, nextState)
项目中有两处用到了componentWillUpdate方法,我们来看一段。1
2
3
4
5componentWillUpdate({changed, field}, {status}){
if(status && !this.state.status || changed != this.props.changed){
this.setState({field});
}
}这里在 componentWillUpdate 中根据 props 的变化去setState。但是 componentWillUpdate有可能在一次更新中被调用多次,在性能方面来说,多次setState在这里是不可取的。而componentDidUpdate一次更新中只会被调用一次,因此我们可以将 componentWillUpdate 中的内容迁移至 componentDidUpdate,改写后的代码如下
1
2
3
4
5
6
7
8componentDidUpdate(prevProps, prevState) {
if (
(!prevState.status && this.state.status) ||
prevProps.changed !== this.props.changed
) {
this.setState({ this.props.field });
}
}componentWillReceiveProps的替换
componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调用。1
2
3
4
5
6
7componentWillReceiveProps(nextProps) {
const { data, list } = nextProps;
this.setState({
data,
list,
});
}从项目中找出一段代码,这里其实应该比较props有变化时再去setState,我们用getDerivedStateFromProps方法来替换
在React16.3中,我们可以这样来改写1
2
3
4
5
6
7
8
9
10//注意,16.4以后版本不能用这种写法, 会产生bug!!!
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.data !== prevState.data || nextProps.list !== prevState.list) {
return {
data: nextProps.data,
list: nextProps.list,
};
}
return null;
}
请注意!!!在 React 16.4^ 的版本中 setState 和 forceUpdate 也会触发getDerivedStateFromProps,所以内部 state 变化后,又会走 getDerivedStateFromProps 方法,并把 state 值更新为传入的 prop。而且getDerivedStateFromProps 和 componentWillReceiveProps 只要父级重新渲染时,这两个生命周期函数就会重新调用,不管 props 有没有“变化”。因此,我们要改变一下判断条件,要注意把传入的 prop 值和之前传入的 prop 进行比较。
1 | static getDerivedStateFromProps (nextProps, prevState) { |
修改完了还有一点需要我们注意,由于componentWillReceiveProps在组件更新时才会调用,而getDerivedStateFromProps在初始挂载及后续更新时都会被调用,因此我们将生命周期替换后,如果didMount里有根据props初始化state的操作可以去掉,可以看到项目中有几处这样的地方。
如果在componentWillReceiveProps中有执行副作用(例如数据获取)以响应 props 中的更改,我们可以改用在componentDidUpdate中触发这些回调。
1 | componentWillReceiveProps(nextProps) { |
改为
1
2
3
4
5componentDidUpdate(prevProps, prevState) {
if (prevState.prevPropData !== this.props.data) {
this.getData(data.type);
}
}
项目中也有很多地方,仅仅是在props更改时重置某些state,没有修改state的业务需求。我们可以从组件里删除state,把它变成一个受控组件。即使仍然有需求要保存临时的值,也可以由父组件像子组件传入一个方法手动执行保存这个动作。亦或者是将组件改为有key的非受控组件,因为当key变化时,React会创建一个新的而不是更新一个既有的组件。
1
<Demo key={item.id} defaultValue={item.value}>
每次id更改,都会重新创建组件,并将其状态重置为最新的value值。这个过程看起来很慢,不过这点性能是可以忽略的。而且如果在组件树的更新上有很重的逻辑,由于子组件的diff过程被省略了,这样反而会更快。
总结
在使用getDerivedStateFromProps方法时,要注意把传入的 prop 值和之前传入的 prop 进行比较,其中之前传入的props通过return存储在state中。在用getDerivedStateFromProps之前可以想一想当前业务场景是否需要用到这个方法,同时保证它是纯函数,不要产生一些副作用。