一、react 16前的生命周期
1.1 constructor
constructor(props, context)
- 如果想(在组件其他地方是可以直接接收的)使用props或context,则需要以参数形式传入。
- 只要组件存在constructor,就必要要写super,否则this指向会错误
1.2 componentWillMount
componentWillMount ()
- 在组件挂载之前调用,且全局只调用一次。如果在这个钩子里可以setState,render后可以看到更新后的state,不会触发重复渲染。
- 该生命周期可以发起异步请求,并setState。(React v16.3后废弃该生命周期,可以在constructor中完成设置state)
- 不推荐在这里发起ajax请求,若返回数据为空,则容易造成界面空白,影响渲染效果
1.3 componentDidMount
componentDidMount ()
- 组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染
1.4 componentWillReceiveProps
componentWillReceiveProps (nextProps)
- 在接受父组件改变后的props需要重新渲染组件时用到的比较多
- 通过对比nextProps和this.props,将nextProps setState为当前组件的state,从而重新渲染组件
1.5 shouldComponentUpdate
shouldComponentUpdate (nextProps, nextState)
- react性能优化非常重要的一环。
- 组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候。
- 首次渲染组件不会调用
1.6 componentWillUpdate
shouldComponentUpdate (nextProps, nextState)
- 组件初始化时不调用,只有在组件发生更新需要重新渲染时才调用
1.7 componentDidUpdate
componentDidUpdate (prevProps, prevState)
- 组件初始化时不调用,组件更新渲染完成后调用,此时dom节点加载完成,可以获取到dom节点。
- react只会在第一次初始化成功会进入componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state。
- 该钩子内setState有可能会触发重复渲染,需要谨慎判断,否则会进入死循环
1.8 render
render()
1.9 componentWillUnmount
componentWillUnmount()
- 组件将要卸载时调用,一些事件监听和定时器需要在此时清除。
1.10 图解
可以将生命周期分为三类:
- mounting:constructor、componentWillMount、render、componentDidMount
- updating: componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、componentDidUpdate
- Unmounting:componentWillUnmount
二、React Fiber
2.1 什么是react fiber
是异步渲染 ui 的解决方案
2.2 出现的背景
16 之前的 React中,更新过程是同步的,这可能会导致性能问题。
当React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,那React就一鼓作气运行到底,中途绝不停歇。
表面上看,这样的设计也是挺合理的,因为更新过程不会有任何I/O操作嘛,完全是CPU计算,所以无需异步操作,的确只要一路狂奔就行了,但是,当组件树比较庞大的时候,问题就来了。
假如更新一个组件需要1毫秒,如果有200个组件要更新,那就需要200毫秒,在这200毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这200毫秒内,用户往一个input元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被React占着呢,抽不出空,最后的结果就是用户敲了按键看不到反应,等React更新过程结束之后,咔咔咔那些按键一下子出现在input元素里了。
这就是所谓的界面卡顿,很不好的用户体验。
现有的React版本,当组件树很大的时候就会出现这种问题,因为更新过程是同步地一层组件套一层组件,逐渐深入的过程,在更新完所有组件之前不停止,函数的调用栈就像下图这样,调用得很深,而且很长时间不会返回。
2.3 React Fiber的方式
破解JavaScript中同步操作时间过长的方法其实很简单——分片。
把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。
React Fiber把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。
维护每一个分片的数据结构,就是Fiber。
有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。
2.4 react fiber对生命周期的影响
在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废
,然后等待机会从头再来
。
因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase
(调节阶段)和第二阶段Commit Phase
。
在第一阶段Reconciliation Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断
的;但是到了第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断
。
这两个阶段大部分工作都是React Fiber做,和我们相关的也就是生命周期函数。
以render函数为界
,第一阶段可能会调用下面这些生命周期函数,说是“可能会调用”是因为不同生命周期调用的函数不同。
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
下面这些生命周期函数则会在第二阶段调用。
- componentDidMount
- componentDidUpdate
- componentWillUnmount
2.5 React Fiber对生命周期函数带来的问题
在 16 前的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用
!
多次调用就有可能带来一些额外的问题,比如在第一阶段的生命周期函数中通过ajax获取数据,就有可能多次调用ajax。
componentWillMount
componentWillReceiveProps
componentWillUpdate
这三个函数都可能出现上面的这个问题,shouldComponentUpdate
这个函数只会返回true or false 不会产生什么副作用,多调几次也没事。
针对这种情况,react 16 之后不提倡使用(17会完全禁用)componentWillMount
componentWillReceiveProps
componentWillUpdate
。
那如果想要在mounting之前操作怎么办?react提供了新的生命周期函数:getDerivedStateFromProps
三、 16 新的生命周期函数
3.1 getDerivedStateFromProps
static getDerivedStateFromProps(nextProps, prevState)
- 根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
- 无论是Mounting还是Updating,也无论是因为什么引起的Updating,全部都会被调用
在使用componentWillReceiveProps
会告警说getDerivedStateFromProps
是componentWillReceiveProps的重命名,这种说法不完成正确: - componentWillReceiveProps:只有因为父组件引发的Updating过程中才被调用
- getDerivedStateFromProps:无论是Mounting还是Updating,也无论是因为什么引起的Updating,全部都会被调用
3.2 getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
- 会在render之后执行,而执行之时DOM元素还没有被更新,给了一个机会去获取DOM信息,计算得到一个snapshot,这个snapshot会作componentDidUpdate的第三个参数传入
3.3 componentDidCatch
componentDidCatch (error, info)
- React 16 中引入,用来 捕获组件的错误。
- 如果 render() 函数抛出错误,则会触发该函数。
- 错误在渲染阶段中被捕获,但在事件处理程序中不会被捕获。
3.4 图解
getDerivedStateFromProps无论是Mounting还是Updating,也无论是因为什么引起的Updating,全部都会被调用。
用一个静态函数getDerivedStateFromProps来取代被deprecate的几个生命周期函数,就是强制开发者在render之前只做无副作用的操作,而且能做的操作局限在根据props和state决定新的state。
3.5 生命周期函数getDerivedStateFromProps混用
如果getDerivedStateFromProps
和componentWillMount
componentWillUpdate
componentWillReceiveProps
混用,控制台会报错,后面三个生命周期将失去作用。
四、代码
import React from 'react';
import Children from './Children';
class Up extends React.Component {
constructor(props){
super(props);
this.state = {
showChildren: true,
msg: "父组件传值给子组件"
}
}
changeFlag = () => {
this.setState(
{
showChildren: !this.state.showChildren
}
)
}
changeMsg = () => {
this.setState({
msg: (new Date()).toString()
})
}
render(){
console.log("render");
return(
<div>
父组件<br /><br /><br />
{ this.state.showChildren ? <Children msg={this.state.msg} /> : "不显示子组件"}<br /><br /><br />
<button onClick={this.changeFlag}>是否显示子组件</button><br />
<button onClick={this.changeMsg}>改变子组件的传值</button>
</div>
)
}
}
export default Up;
import React from 'react';
class Children extends React.Component{
constructor(props, context) {
//如果想(在组件其他地方是可以直接接收的)使用props或context,则需要以参数形式传入。
//只要组件存在constructor,就必要要写super,否则this指向会错误
super(props, context);
console.log("constructor");
this.state = {
data: this.props.msg
};
};
componentWillMount () {
// 在组件挂载之前调用,且全局只调用一次。如果在这个钩子里可以setState,render后可以看到更新后的state,不会触发重复渲染。
// 该生命周期可以发起异步请求,并setState。(React v16.3后废弃该生命周期,可以在constructor中完成设置state)
// 不推荐在这里发起ajax请求,若返回数据为空,则容易造成界面空白,影响渲染效果
console.log("componentWillMount");
};
componentDidMount () {
//组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染
console.log("componentDidMount");
};
componentWillReceiveProps (nextProps) {
// 在接受父组件改变后的props需要重新渲染组件时用到的比较多
// 通过对比nextProps和this.props,将nextProps setState为当前组件的state,从而重新渲染组件
console.log("componentWillReceiveProps");
this.setState({
data: nextProps.msg
});
};
shouldComponentUpdate (nextProps, nextState) {
// react性能优化非常重要的一环。
// 组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,
// 如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,
// 这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候。
console.log("shouldComponentUpdate");
return true;
};
componentWillUpdate (nextProps, nextState) {
// 组件初始化时不调用,只有在组件发生更新需要重新渲染时才调用
console.log("componentWillUpdate");
};
componentDidUpdate (prevProps, prevState) {
// 组件初始化时不调用,组件更新渲染完成后调用,此时dom节点加载完成,可以获取到dom节点。
// react只会在第一次初始化成功会进入componentDidmount,
// 之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state。
// 该钩子内setState有可能会触发重复渲染,需要谨慎判断,否则会进入死循环
console.log("componentDidUpdate");
};
changeData = () => {
console.log("changeData");
this.setState({
data: (new Date()).toString()
})
}
render () {
console.log("render");
return (
<div>
子组件!<br />
<button onClick={this.changeData}> 改变state中的值</button>
</div>
);
};
componentWillUnmount () {
// 组件将要卸载时调用,一些事件监听和定时器需要在此时清除。
console.log("componentWillUnmount");
};
}
export default Children;
import React from 'react';
class Children extends React.Component{
constructor(props, context) {
//如果想(在组件其他地方是可以直接接收的)使用props或context,则需要以参数形式传入。
//只要组件存在constructor,就必要要写super,否则this指向会错误
super(props, context);
console.log("constructor");
this.state = {
data: this.props.msg
};
};
componentWillMount () {
// 在组件挂载之前调用,且全局只调用一次。如果在这个钩子里可以setState,render后可以看到更新后的state,不会触发重复渲染。
// 该生命周期可以发起异步请求,并setState。(React v16.3后废弃该生命周期,可以在constructor中完成设置state)
// 不推荐在这里发起ajax请求,若返回数据为空,则容易造成界面空白,影响渲染效果
console.log("componentWillMount");
};
componentDidMount () {
//组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染
console.log("componentDidMount");
};
static getDerivedStateFromProps(nextProps, preState){
//根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
//无论是Mounting还是Updating,也无论是因为什么引起的Updating,全部都会被调用
console.log("getDerivedStateFromProps")
return {
msg: (new Date()).toString()
}
}
componentWillReceiveProps (nextProps) {
// 在接受父组件改变后的props需要重新渲染组件时用到的比较多
// 通过对比nextProps和this.props,将nextProps setState为当前组件的state,从而重新渲染组件
console.log("componentWillReceiveProps");
this.setState({
data: nextProps.msg
});
};
shouldComponentUpdate (nextProps, nextState) {
// react性能优化非常重要的一环。
// 组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,
// 如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,
// 这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候。
console.log("shouldComponentUpdate");
return true;
};
componentWillUpdate (nextProps, nextState) {
// 组件初始化时不调用,只有在组件发生更新需要重新渲染时才调用
console.log("componentWillUpdate");
};
componentDidUpdate (prevProps, prevState) {
// 组件初始化时不调用,组件更新渲染完成后调用,此时dom节点加载完成,可以获取到dom节点。
// react只会在第一次初始化成功会进入componentDidmount,
// 之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state。
// 该钩子内setState有可能会触发重复渲染,需要谨慎判断,否则会进入死循环
console.log("componentDidUpdate");
};
changeData = () => {
console.log("changeData");
this.setState({
data: (new Date()).toString()
})
}
render () {
console.log("render");
return (
<div>
子组件!<br />
<button onClick={this.changeData}> 改变state中的值</button>
</div>
);
};
componentWillUnmount () {
// 组件将要卸载时调用,一些事件监听和定时器需要在此时清除。
console.log("componentWillUnmount");
};
}
export default Children;