componentWillMount
、
componentWillReceiveProps
、
componentWillUpdate
三个生命周期函数都有eslint报警,让我们使用
UNSAFE_
前缀的新的生命周期函数。不禁有疑问“react这是意欲何为啊”?为什么要加
UNSAFE_
前缀?
为什么要对这些生命周期函数报警?
1、componentWillMount
componentWillMount
生命周期发生在首次渲染前,一般使用的小伙伴大多在这里初始化数据或异步获取外部数据赋值。初始化数据,react官方建议放在constructor
里面。而异步获取外部数据,渲染并不会等待数据返回后再去渲染。
案例一:如下是安装时监听外部事件调度程序的组件示例
class Example extends React.Component {
state = {
value: ''
};
componentWillMount() {
this.setState({
value: this.props.source.value
});
this.props.source.subscribe(this.handleChange);
}
componentWillUnmount() {
this.props.source.unsubscribe(this.handleChange );
}
handleChange = source => {
this.setState({
value: source.value
});
};
}
复制代码
试想一下,假如组件在第一次渲染的时候被中断,由于组件没有完成渲染,所以并不会执行componentWillUnmount
生命周期(注:很多人经常认为componentWillMount和componentWillUnmount总是配对,但这并不是一定的。只有调用componentDidMount后,React才能保证稍后调用componentWillUnmount进行清理
)。因此handleSubscriptionChange
还是会在数据返回成功后被执行,这时候setState
由于组件已经被移除,就会导致内存泄漏。所以建议把异步获取外部数据写在componentDidMount
生命周期里,这样就能保证componentWillUnmount
生命周期会在组件移除的时候被执行,避免内存泄漏的风险。
现在,小伙伴清楚为什么了要用UNSAFE_componentWillMount
替换componentWillMount
了吧(注意:这里的UNSAFE并不是指安全性,而是表示使用这些生命周期的代码将更有可能在未来的React版本中存在缺陷,特别是一旦启用了异步渲染
)
2、componentWillReceiveProps
componentWillReceiveProps
生命周期是在props更新时触发。一般用于props
参数更新时同步更新state参数。但如果在componentWillReceiveProps
生命周期直接调用父组件的某些有调用setState
的函数,会导致程序死循环。
案例二:如下是子组件componentWillReceiveProps
里调用父组件改变state的函数示例
...
class Parent extends React.Component{
constructor(){
super();
this.state={
list: [],
selectedData: {}
};
}
changeSelectData = selectedData => {
this.setState({
selectedData
});
}
render(){
return (
<Clild list={this.state.list} changeSelectData={this.changeSelectData}/>
);
}
}
...
class Child extends React.Component{
constructor(){
super();
this.state={
list: []
};
}
componentWillReceiveProps(nextProps){
this.setState({
list: nextProps.list
})
nextProps.changeSelectData(nextProps.list[0]); //默认选择第一个
}
...
}
复制代码
如上代码,在Child
组件的componentWillReceiveProps
里直接调用Parent
组件的changeSelectData
去更新Parent
组件state
的selectedData
值。会触发Parent
组件重新渲染,而Parent
组件重新渲染会触发Child
组件的componentWillReceiveProps
生命周期函数执行。如此就会陷入死循环。导致程序崩溃。
所以,React官方把componentWillReceiveProps
替换为UNSAFE_componentWillReceiveProps
,让小伙伴在使用这个生命周期的时候注意它会有缺陷,要注意避免,比如上面例子,Child
在componentWillReceiveProps
调用changeSelectData
时先判断list
是否有更新再确定是否要调用,就可以避免死循环。
3、componentWillUpdate
componentWillUpdate
生命周期在视图更新前触发。一般用于视图更新前保存一些数据方便视图更新完成后赋值。 案例三:如下是列表加载更新后回到当前滚动条位置的案例
class ScrollingList extends React.Component {
listRef = null;
previousScrollOffset = null;
componentWillUpdate(nextProps, nextState) {
if (this.props.list.length < nextProps.list.length) {
this.previousScrollOffset = this.listRef.scrollHeight - this.listRef.scrollTop;
}
}
componentDidUpdate(prevProps, prevState) {
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
和componentDidUpdate
这两个生命周期函数有一定的时间差(componentWillUpdate
后经过渲染、计算、再更新DOM
元素,最后才调用componentDidUpdate
),如果这个时间段内用户刚好拉伸了浏览器高度,那componentWillUpdate
计算的previousScrollOffset
就不准确了。如果在componentWillUpdate
进行setState
操作,会出现多次调用只更新一次的问题,把setState放
在componentDidUpdate
,能保证每次更新只调用一次。
所以,react官方建议把componentWillUpdate
替换为UNSAFE_componentWillUpdate
。如果真的有以上案例的需求,可以使用16.3新加入的一个周期函数getSnapshotBeforeUpdate
。下面会有具体说明,这里暂时卖个关子。
有什么替换方案?
1、getDerivedStateFromProps
getDerivedStateFromProps
是官方在16.3新加入的生命周期函数,props
变化时被调用,若是父组件重新渲染,也会被调用。它返回新的props
值。
案例四:如下是getDerivedStateFromProps
的使用实例
class Example extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.name !== prevState.name) {
return {
name: nextProps.name
}
}
}
}
复制代码
可以看到,getDerivedStateFromProps
接收最新的Props
值nextProps
、上一个state
值prevState
两个参数,返回返回一个对象来更新state
,或者返回null
表示不需要更新state
。要注意的是,getDerivedStateFromProps
不能访问this
,所以如果要跟上一个props
值作比较,只能是把上一个props
值存到state
里作为镜像。到这里你一定有疑问,为什么不把上一个props
值传给getDerivedStateFromProps
?官方给的解析如下:
-
在第一次调用
getDerivedStateFromProps
(实例化后)时,prevProps
参数将为null
,需要在访问prevProps
时添加if-not-null
检查。 -
没有将以前的
props
传递给这个函数,在未来版本的React中释放内存的一个步骤。 (如果React不需要将先前的道具传递给生命周期,那么它不需要将先前的道具对象保留在内存中。)
综上可知,getDerivedStateFromProps
正是官方新加入的用以替代componentWillReceiveProps
的方案。如果说,你的项目会考虑往后的版本兼容,建议改用getDerivedStateFromProps
。
2、getSnapshotBeforeUpdate
getSnapshotBeforeUpdate
是跟getDerivedStateFromProps
一起,在16.3新加入的生命周期函数。触发的时机在最近的更改被提交到DOM
元素前,使得组件可以在更改之前获得当前值,此生命周期返回的任意值都会作为第三个参数传给componentDidUpdate
。一般当我们需要在更新DOM
前需要保存DOM
当前的状态时会使用这个生命周期,比较常见是用于DOM
更新前获取滚动位置,更新后恢复到该滚动位置。比如上面的案例三,componentWillUpdate
更好的替换方案就是getSnapshotBeforeUpdate
,getSnapshotBeforeUpdate
到componentDidUpdate
只经过了更新DOM
这一操作。
案例五:如下为案例三的更好的替换方案
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevProps.list.length < this.props.list.length) {
return this.listRef.scrollHeight - this.listRef.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
this.listRef.scrollTop = this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
`<div>` {/* ...contents... */}`</div>`
);
}
setListRef = ref => { this.listRef = ref; };
}
复制代码
最后,关于componentWillMount
的替换方案,官方建议把该生命周期函数的逻辑处理放到componentDidMount
里面去。
随着React版本迭代,会否兼容UNSAFE类生命周期
React官网上的计划是:
-
16.3:为不安全生命周期引入别名
UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
和UNSAFE_componentWillUpdate
。 (旧的生命周期名称和新的别名都可以在此版本中使用。) -
未来的16.x版本:为
componentWillMount
,componentWillReceiveProps
和componentWillUpdate
启用弃用警告。 (旧的生命周期名称和新的别名都可以在此版本中使用,但旧名称会记录DEV模式警告。) -
17.0:删除
componentWillMount
,componentWillReceiveProps
和componentWillUpdate
。 (从现在开始,只有新的“UNSAFE_”生命周期名称将起作用。)
结论
其实,说了这么多。就两点:
1、React意识到componentWillMount
、componentWillReceiveProps
和componentWillUpdate
这三个生命周期函数有缺陷,比较容易导致崩溃。但是由于旧的项目已经在用以及有些老开发者习惯用这些生命周期函数,于是通过给它加UNSAFE_
来提醒用它的人要注意它们的缺陷。
2、React加入了两个新的生命周期函数getSnapshotBeforeUpdate
和getDerivedStateFromProps
,目的为了即使不使用这三个生命周期函数,也能实现只有这三个生命周期能实现的功能。
ps:本文部分内容借鉴参考文章ReactV16.3即将更改的生命周期