对于react生命周期的理解,反反复复有很多次不同的理解,我就做个整理,以免每次都进行重新推翻
按照官网的解释组件的生命周期分成挂载,更新,卸载,以及错误处理的几个流程
16.3以前的生命周期分成
- 初始化阶段constuctor,
- 挂载阶段有componentWillMount,render ,componentDidMount
- 更新阶段的流程是componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate, render,componentDidUpdate
- 卸载 阶段componentWillunMount
- 错误处理 componentDidCatch()
16.4推出fibber之后,官网也说将在17.0开启async rendering(异步渲染)
那么render之前的函数都将被执行多次 所以
16.3以后新增getDerivedStateFromProp将逐渐替代render之前(除shouldComponentUpdate)的生命(componentWillMount,componentWillUpdate, componentWillReceiveProps)
- 挂载阶段 constructor, getDerivedStateFromProps render componentDidMount
- 更新阶段 getDerivedStateFromProps shouldComonentUpdate render getSnapshotBeforeUpdate componentDidUpdate
- 错误处理 static getDerivedStateFromError() static getDerivedStateFromError()
componentWillMount
该生命是在组件挂载到dom之前会被调用 只调用一次,官网指明在这里用setstate不会引起组件重新渲染dom,此方法是服务端渲染唯一会调用的生命周期函数
(我试了下在componentsWillMount直接调用setState。是可以让渲染后拿到最新的state的值,但是此时render只调用一次。就说明这里用setstate不会引起组件重新(第二次)渲染dom。只是在render之前setState已经将需要更新的加入队列了。)这样不算重新触发渲染的更新是没什么意义,而这样的初始化state应该放在constuctor里面
componentDiDMount
组件挂载后,(插入到Dom树)之后会被调用。官网建议网络数据请求,最适合放在这里。依赖dom节点的初始化应该放在这个生命周期
但是不适合直接在这里调用setState(),因为componentDidMount本身处于一次更新中,我们又调用了一次setstate 就会在未来在执行一次render 造成不必要的性能浪费。所以不推荐直接在关于componentDidMount调用setstate。 但是在componentDidMount可以条用接口,在回调中去修改setstate。
官网也指明说,两次渲染会发生在浏览器更新屏幕之前,但是不推荐。会导致性能问题。
关于在哪个生命周期发起异步请求获取页面初始数据
。
那如果在这里发送异步请求拉去数据并且setState更新数据呢,是不是可以比在componentDidMount减少一次渲染,然后优先提早拿到更新的数据呢?(官网不推荐)
state={
count:0
}
componentWillMount(){
console.log('willMount')
fetch('s.codepen.io')
.then(res =>{
this.setState({count: 'success'})
console.log('setdata')
})
.catch(err => this.setState({count: 'error'}))
}
componentDidMount(){
console.log('didMount')
}
render() {
console.log('render')
return <div>{this.state.count}</div>
}
//页面最后显示success 打印结果
// willMount
// render
// didMount
//setdata
//render
可以看出,render是在componentWillMount执行之后马上就被调用,所以此时由于异步请求还没有拿到数据。等到异步请求拿到数据之后去setState。会重新调用render,总的来说还是进行了两次渲染,异步请求之后的setState还是触发了渲染更新。所以初始需要请求异步数据,放在这里也同样需要render一次“加载中”的空数据状态。总的来说,组件在首次渲染时总是会处于没有异步数据的状态。
那么为什么建议在componentDiDMount异步获取外部数据呢?
1、如果是服务端渲染,componentWillMount是唯一会执行的生命周期,如果是服务端渲染,在这里获取数据(发送请求)可能会执行两次。一次是在服务端一次是在客户端
2、如果在16.4之后增加了fiber,使的整个React的生命周期分成两个阶段,在第一阶段的生命周期是可以被中断的,每次中断之后都会重新执行第一阶段得,而第二阶段不能中断。一旦触发第二阶段,就一定要等到第二阶段执行完毕,componentWillMount在第一阶段,componentDidMount在第二阶段,如果吧请求放在componentWillMount中则可能发送多次请求,
综上所述所以放在componentDidMount中更合适
-
关于事件订阅
一般情况下,如果在componentWillMount 中做订阅外部事件,会在componentWillunMount中取消订阅,但是在服务端渲染的情况下,服务端是不会调用componentWillunMount,所以在服务端订阅事件是会导致内存泄露。
另一方面听上面的问题一样。在未来开启React异步渲染之后,第一阶段componentWillMount调用之后,组件的渲染还是有可能被其他事物中断的,所以没有办法保证componentWillunMount可以被调用 。。
componentDidMount不会有这个问题 所以添加订阅也应该在componentDidMount中
componentWillReceiveProps
componentWillReceiveProps(nextProps)
调用时机:只有在父组件重新渲染的时候(就是已挂载的组件接收到新的props之前,(此时this.props访问到的还是渲染之前的props))调用,不管父组件传来的props有没有改变。只要父组件重新渲染都会调用此方法、
getDerivedStateFromProps
getDerivedStateFromProps(props,state)
是静态方法,无权访问组件实例,(即使无法使用this)在state更新或者props更新的时候都会调用组件,就是每次渲染前都会调用,这个与componentWillReceiveProps不同。此方法适用于罕见案例,就是state的值在任何情况下都去取决于props。返回一个对象用来更新state,返回null不更新任何内容
需要优化的点:
-
基于 props 更新 state
一般如果是组件的state的值任何情况下都依赖于props的时候,在16.3以后应该抛弃componentWillReceiveProps,而使用getDerivedStateFromProps返回一个对象用来更新state.
static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
// 返回 null 表示无需更新 state。
return null;
}
-
props 更新时获取外部数据,props 更新的副作用
如果需要更新状态以响应props的更改,则可以通过用this.props 和nextProps进行比较。在挂载的过程中,不会针对初始的props去调用改方法,
官方指出,如果要执行副作用(数据提取和动画)请改用componentDidUpdate ,在这之前很多时候都会用到redux存放props。如果有props更新引起的副作用。所以就会有
之前16.3之前大多数用
class ExampleComponent extends React.Component {
componentWillReceiveProps(nextProps) {
if (this.props.isVisible !== nextProps.isVisible) {
this._loadAsyncData(nextProps.isVisible);
}
}
}
static getDerivedStateFromProps(props, state) {
// 保存 prevId 在 state 中,以便我们在 props 变化时进行对比。
// 清除之前加载的数据(这样我们就不会渲染旧的内容)。
if (props.id !== state.prevId) {
return {
externalData: null,
prevId: props.id,
};
}
// 无需更新 state
return null;
}
componentDidUpdate(prevProps, prevState) {
if (this.state.externalData === null) {
this._loadAsyncData(this.props.id);
}
}
与 componentWillUpdate 类似,componentWillReceiveProps 可能在一次更新中被多次调用,也就是说写在这里的副作用方法,异步请求,回调函数也有可能会被调用多次,而此时与 componentDidMount 类似,componentDidUpdate 也不存在这样的问题,一次更新中 componentDidUpdate 只会被调用一次,所以官网建议讲 componentDidUpdate 就可以解决这个问题
- 同样的props更新引起的副作用也应该从componentWillReceiveProps 迁移到componentDidUpdate
- 以及props更新引起的调用外部回调。也应该从componentWillUpdate迁移至componentDidUpdate
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
调用时机:会在最终的render之前被调用,也就是getSnapshotBeforeUpdate中获取到的dom元素状态与componentWillUpdate的是一样的,所以可以用 这个方法代替componentWillUpdate获取组件更改之前捕获一些dom信息(例如:滚动高度、)
返回的一个值作为componentDidUpdate, 的第三个参数。
配个componentDidUpdate。覆盖componentWillUpdate的用法
-优化的点:在更新前记录获取原来的dom节点属性
在没有这个生命周期之前,一般会利用在componentWillUpdate读取更新前dom元素状态属性,但是在异步渲染中,render阶段的生命周期(如 componentWillUpdate 和 render)和commoit阶段的生命周期“”(componentDidUpdate)可能存在延迟、
官方提供了下面这个例子
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
参考官方文档
[https://react.docschina.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data]