在说新的生命周期之前,我们先了解下原来的生命周期:
- 挂载卸载过程
1.1.constructor()
constructor()中完成了React数据的初始化,它接受两个参数:props和context,当想在函数内部使用这两个参数时,需使用super()传入这两个参数。
注意:只要使用了constructor()就必须写super(),否则会导致this指向错误。
1.2.componentWillMount()
componentWillMount()一般用的比较少,它更多的是在服务端渲染时使用。它代表的过程是组件已经经历了constructor()初始化数据后,但是还未渲染DOM时。
1.3.componentDidMount()
组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染
1.4.componentWillUnmount ()
在此处完成组件的卸载和数据的销毁。
- clear你在组建中所有的setTimeout,setInterval
- 移除所有组建中的监听 removeEventListener
- 有时候我们会碰到这个warning:
Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.
原因:因为你在组件中的ajax请求返回setState,而你组件销毁的时候,请求还未完成,因此会报warning
解决方法:
componentDidMount() {
this.isMount === true
axios.post().then((res) => {
this.isMount && this.setState({ // 增加条件ismount为true时
aaa:res
})
})
}
componentWillUnmount() {
this.isMount === false
}
- 更新过程
2.1. componentWillReceiveProps (nextProps)
- 在接受父组件改变后的props需要重新渲染组件时用到的比较多
- 接受一个参数nextProps
- 通过对比nextProps和this.props,将nextProps的state为当前组件的state,从而重新渲染组件
componentWillReceiveProps (nextProps) {
nextProps.openNotice !== this.props.openNotice&&this.setState({
openNotice:nextProps.openNotice
},() => {
console.log(this.state.openNotice:nextProps)
//将state更新为nextProps,在setState的第二个参数(回调)可以打 印出新的state
})
}
2.2.shouldComponentUpdate(nextProps,nextState)
- 主要用于性能优化(部分更新)
- 唯一用于控制组件重新渲染的生命周期,由于在react中,setState以后,state发生变化,组件会进入重新渲染的流程,在这里return false可以阻止组件的更新
- 因为react父组件的重新渲染会导致其所有子组件的重新渲染,这个时候其实我们是不需要所有子组件都跟着重新渲染的,因此需要在子组件的该生命周期中做判断
2.3.componentWillUpdate (nextProps,nextState)
shouldComponentUpdate返回true以后,组件进入重新渲染的流程,进入componentWillUpdate,这里同样可以拿到nextProps和nextState。
2.4.componentDidUpdate(prevProps,prevState)
组件更新完毕后,react只会在第一次初始化成功会进入componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state。
2.5.render()
render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。
接下来我们看看更新后的生命周期
先看看它的变化:
新增:getDerivedStateFromProps,getSnapshotBeforeUpdate
UNSAFE:UNSAFE_componentWillMount,UNSAFE_componentWillUpdate,UNSAFE_componentWillReceiveProps
React 官方正式发布了 v16.3 版本。在这次的更新中,除了前段时间被热烈讨论的新 Context API 之外,新引入的两个生命周期函数 getDerivedStateFromProps,getSnapshotBeforeUpdate 以及在未来 v17.0 版本中即将被移除的三个生命周期函数 componentWillMount,componentWillReceiveProps,componentWillUpdate .
getDerivedStateFromProps
React生命周期的命名一直都是非常语义化的,这个生命周期的意思就是从props中获取state,可以说是太简单易懂了。可以说,这个生命周期的功能实际上就是将传入的props映射到state上面。由于16.4的修改,这个函数会在每次re-rendering之前被调用,这意味着什么呢?意味着即使你的props没有任何变化,而是父state发生了变化,导致子组件发生了re-render,这个生命周期函数依然会被调用。看似一个非常小的修改,却可能会导致很多隐含的问题。
使用
这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在你需要使用componentWillReceiveProps的时候,就可以考虑使用getDerivedStateFromProps来进行替代了。
两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过this访问到class的属性,也并不推荐直接访问属性。而是应该通过参数提供的nextProps以及prevState来进行判断,根据新传入的props来映射到state。
需要注意的是,如果props传入的内容不需要影响到你的state,那么就需要返回一个null,这个返回值是必须的,所以尽量将其写到函数的末尾。
// 在getDerivedStateFromProps中进行state的改变
static getDerivedStateFromProps({ error, value }, state) {
/* Keep last received error in state */
if (error && error !== state.error) {
return { error };
}
if (value && value !== state.text) {
return { text: value }
}
return null;
}
Case1 -- 多来源的不同状态
假设我们有一个列表,这个列表受到页面主体,也就是根组件的驱动,也受到其本身数据加载的驱动。
因为这个页面在开始渲染的时候,所有的数据请求可能是通过batch进行的,所以要在根组件进行统一处理,而其列表的分页操作,则是由其本身控制。
这会出现什么问题呢?该列表的状态受到两方面的控制,也就是re-render可能由props驱动,也可能由state驱动。这就导致了getDerivedStateFromProps会在两种驱动状态下被重新渲染。
当这个函数被多次调用的时候,就需要注意到,state和props的变化将会怎样影响到你的组件变化。
// 组件接收一个type参数
static propTypes = {
type: PropTypes.number
}
// 组件还具有自己的状态来渲染列表
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
type: 0,
}
}
}
如上面代码的例子所示,组件既受控,又控制自己。当type发生变化,会触发一次getDerivedStateFromProps
,在这里更新组件的type状态,然而,在进行异步操作之后,组件又会更新list状态,这时你的getDerivedStateFromProps
函数就需要注意,不能够仅仅判断type是否变化来更新状态,因为list的变化也会更新到组件的状态。这时就必须返回一个null,否则会导致组件无法更新并且报错。
Case2 -- 组织好你的组件
考虑一下,如果你的组件内部既需要修改自己的type,又需要接收从外部修改的type。
是不是非常混乱?getDerivedStateFromProps中你根本不知道该做什么。
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// type可能由props驱动,也可能由state驱动,这样判断会导致state驱动的type被回滚
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}
如何解决这个棘手的问题呢?
好好组织你的组件,在非必须的时候,摒弃这种写法。type要么由props驱动,要么完全由state驱动。
如果实在没有办法解耦,那么就需要一个hack来辅助:绑定props到state上。
constructor(props) {
super(props);
this.state = {
type: 0,
props,
}
}
static getDerivedStateFromProps(nextProps, prevState) {
const {type, props} = nextProps;
// 这段代码可能看起来非常混乱,这个props可以被当做缓存,仅用作判断
if (type !== props.type) {
return {
type,
props: {
type,
},
};
}
// 否则,对于state不进行任何操作
return null;
}
上面的代码可以保证在进行多数据源驱动的时候,状态能够正确改变。当然,这样的代码很多情况下是会影响到别人阅读你的代码的,对于维护造成了非常大的困难。
从这个生命周期的更新来看,react更希望将受控的props
和state
进行分离,就如同Redux
作者Dan Abramov在redux文档当中写的一样Presentational and Container Components,将所有的组件分离称为展示型组件和容器型组件,一个只负责接收props
来改变自己的样式,一个负责保持其整个模块的state
。这样可以让代码更加清晰。但是在实际的业务逻辑中,我们有时很难做到这一点,而且这样可能会导致容器型组件变得非常庞大以致难以管理,如何进行取舍还是需要根据实际场景决定的。
Case3 -- 异步
以前,我们可以在props发生改变的时候,在componentWillReceiveProps中进行异步操作,将props的改变驱动到state的改变。
componentWillReceiveProps(nextProps) {
if (props.type !== nextProps.type) {
// 在这里进行异步操作或者更新状态
this.setState({
type: props.type,
});
this._doAsyncOperation();
}
}
这样的写法已经使用了很久,并且并不会存在什么功能上的问题,但是将componentWillReceiveProps标记为deprecated的原因也并不是因为功能问题,而是性能问题。
当外部多个属性在很短的时间间隔之内多次变化,就会导致componentWillReceiveProps被多次调用。这个调用并不会被合并,如果这次内容都会触发异步请求,那么可能会导致多个异步请求阻塞。
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.
这个生命周期函数会在每次调用render之前被触发,而读过一点react源码的童鞋都会了解,reactsetState操作是会通过transaction进行合并的,由此导致的更新过程是batch的,而react中大部分的更新过程的触发源都是setState,所以render触发的频率并不会非常频繁(感谢 @leeenx20 的提醒,这里描述进行了修改)。
在使用getDerivedStateFromProps的时候,遇到了上面说的props在很短的时间内多次变化,也只会触发一次render,也就是只触发一次getDerivedStateFromProps。这样的优点不言而喻。
那么如何使用getDerivedStateFromProps进行异步的处理呢?
If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.
官方教你怎么写代码系列,但是其实也没有其他可以进行异步操作的地方了。为了响应props的变化,就需要在componentDidUpdate中根据新的props和state来进行异步操作,比如从服务端拉取数据。
// 在getDerivedStateFromProps中进行state的改变
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.type !== prevState.type) {
return {
type: nextProps.type,
};
}
return null;
}
// 在componentDidUpdate中进行异步操作,驱动数据的变化
componentDidUpdate() {
this._loadAsyncData({...this.state});
}
getSnapshotBeforeUpdate(prevProps, prevState)
这个新更新代替componentWillUpdate。
常见的 componentWillUpdate 的用例是在组件更新前,读取当前某个 DOM 元素的状态,并在 componentDidUpdate 中进行相应的处理。
这两者的区别在于:
在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在
componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。
此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。
小结
react为了防止部分开发者滥用生命周期,可谓非常尽心尽力了。既然你用不好,我就干脆不让你用。一个静态的生命周期函数可以让状态的修改更加规范和合理。
React 16是最近一年多React更新最大的版本。推荐向下兼容的Fiber,哈哈,本人还没仔细看,据说是防止了客户端react在进行渲染的时候阻塞页面的其他交互行为。Fiber源码速览