先看旧生命周期的整体流程图
首先要注意,只有类组件有生命周期,函数式组件只能通过hooks实现类似的功能。
1.初始化阶段
1.1 componentWillMount
该函数在react16.2后已被建议弃用。因为老的diff算法在对比虚拟DOM和真实DOM时是同步的,万一对比量过大容易造成性能影响。
16.2推出了新的生命周期,更新了diff算法,提出了fiber技术。此时componentWillMount属于低优先级任务,容易被高优先级任务中断,需要重新执行,具有隐患。
第一次render之前最后一次修改状态的机会,主要作用是进行数据初始化。
1.2 render
只能访问this.props和this.state,不允许修改状态和dom输出
1.3 componentDidMount
成功render并渲染完成真实dom后触发,可以修改dom。
适合用于
- 数据请求 比如axios
- 订阅函数调用
- setInterval
- 基于创建完的dom进行初始化
来看个案例,BetterScroll需要基于创建完的DOM来工作,因此写在componentDidMount中。
export default class App extends Component {
state = {
list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
}
UNSAFE_componentWillMount() {
console.log(document.querySelectorAll('li'))//获取到一个空数组
}
render() {
return (
<div>
<div id='wrapper' style={{height:'150px',background:'lightgreen',overflow:'hidden'}}>
<ul>
{
this.state.list.map(item =>
<li key={item}>{item}</li>
)
}
</ul>
</div>
</div>
)
}
componentDidMount() {
console.log(document.querySelectorAll('li'))//获取到10个li
// 要在这里new 因为此时已经获取到dom节点
new BetterScroll('#wrapper')
}
}
2. 运行(更新)阶段
2.1 componentWillUpdate
用的机会不多,不像Mount可以用来初始化数据,在Update阶段初始化数据会导致死循环。(render后又update状态->状态修改后又触发render...)
类似于componentWillMount,也是不安全的,建议弃用。
2.2 render
和初始化阶段一样,只能访问this.props和this.state,不允许修改状态和dom输出
2.3 componentDidUpdate
类似componentDidMount,作用是更新后用来获取DOM节点
看个小例子,在componentDidMount中用axios异步获取数据时需要将betterscroll写在setState的第二个参数中作为回调函数执行。
componentDidMount() {
axios.get(`/test.json`).then(res => {
this.setState({
filmList: res.data.data.films
}, () => {
new BetterScroll('#wrapper')
})
})
}
可以不在setState中写,直接在axios获取到数据并更新状态后的componentDidUpdate中写,效果相同。
componentDidUpdate也有对应的缺点,就是会执行多次,每次更新状态都会使得它重新执行。
componentDidMount() {
axios.get(`/test.json`).then(res => {
this.setState({
filmList: res.data.data.films
})
})
}
componentDidUpdate() {
new BetterScroll('#wrapper')
}
该生命周期可以传入两个参数,用于记录老的属性和老的状态
componentDidUpdate(prevProps , prevState) {
console.log(prevState.filmList) //空数组[]
if(prevState.filmList.length === 0){
new BetterScroll('#wrapper')
}
}
2.4 shouldComponentUpdate
如果该方法返回false,那么就会阻止render调用
举个例子,第一次点击按钮后myname会改变,但是之后每次点击按钮,虽然state值没有改变,但还是会继续触发虚拟dom和真实dom的对比,render重新渲染页面。
export default class App extends Component {
state = {
myname: 'yoimiya',
}
render() {
return (
<div>
<button onClick={() => {
this.setState({
myname: 'eula'
})
}}>click</button>
<span id='myname'>{this.state.myname}</span>
</div>
)
}
}
该生命周期也是可以传两个参数,分别代表新属性和新状态
// this.state 老的状态
// nextState 新的状态
shouldComponentUpdate(nextProps , nextState){
if(this.state.myname !== nextState.myname){
// 允许render调用
return true;
}else{
// 不允许render调用
return false;
}
}
为了不一一对比对象中的属性,直接将对象序列化后对比两个整体对象。
// this.state 老的状态
// nextState 新的状态
shouldComponentUpdate(nextProps , nextState){
if(JSON.stringify(this.state) !== JSON.stringify(nextState)){
return true;
}else{
return false;
}
}
该生命周期对于性能优化有着重要的作用,举个例子:一个父组件底下10个子组件,根据父组件状态修改其中1个子组件会导致剩下的也要重新渲染,大大影响性能。
先来看没用shouldComponentUpdate优化的代码,根据输入框的value修改current状态,将current和index传给子组件做对比,相等时背景色改变:
class Box extends Component {
render() {
return (
<div style={{
width: '100px', height: '100px', border: '1px solid grey', margin: '10px', float: 'left',
// 根据条件来决定背景色
background: this.props.current === this.props.index ? 'lightgreen' : ''
}}>
</div>
)
}
}
export default class App extends Component {
state = {
list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
current: 0
}
render() {
return (
<div>
{/* 根据输入框内容修改current 记得用Number()转换 */}
<input type='number' onChange={(e) =>{
this.setState({
current : Number(e.target.value)
})
}}></input>
<div style={{ overflow: 'hidden' }}>
{
this.state.list.map((item, index) =>
// 将current和index传给子组件
<Box key={item} current={this.state.current} index={index}></Box>
)
}
</div>
</div>
)
}
}
但是这份代码有个问题,每次修改current,10个Box都会被render重新渲染一次,造成性能影响。
因此可以在Box组件里加入这段代码,实现了每次render只更新2个(老的和新的)需要被更新的Box:
shouldComponentUpdate(newProps){
// 只允许render更新老的Box:选中->未选中,和新的Box:未选中->选中
if(this.props.current === this.props.index || newProps.current === newProps.index){
return true;
}
return false;
}
2.5 componentWillReceiveProps
此生命周期也已经被弃用
父组件修改属性时触发,因此应该写在子组件内。
但是其实并不需要将属性传给孩子,哪怕只是在父组件中更新状态,也会导致子组件的生命周期更新,也会触发该方法。
这个生命周期唯一作用是最先获得父组件传来的属性,可以利用属性进行相应的操作
通过形参中的nextProps(名字不固定),来获取最新的属性
比如这个例子
class Child extends Component {
render() {
return (
<div>
child
</div>
)
}
componentWillReceiveProps(nextProps){
console.log(this.props.text , nextProps.text)
// 111 222
}
}
export default class App extends Component {
state = {
text : '111'
}
render() {
return (
<div>
{
this.state.text
}
<button onClick={() =>{
this.setState({
text : '222'
})
}}>click</button>
<Child text={this.state.text}></Child>
</div>
)
}
}
3. 销毁阶段
销毁阶段就一个函数:componentWillUnmount
作用是在移除组件之前进行清理操作,比如计时器和事件监听器。
export default class App extends Component {
state = {
isCreated: true
}
render() {
return (
<div>
<button onClick={() => {
this.setState({
isCreated: !this.state.isCreated
})
}}>click</button>
{this.state.isCreated && <Child />}
</div>
)
}
}
class Child extends Component {
render() {
return <div>
child
</div>
}
componentDidMount() {
window.onresize = () => {
console.log("resize")
}
// this是Child组件的一个实例
this.timer = setInterval(() => {
console.log("111")
}, 1000)
}
componentWillUnmount() {
console.log("componentWillUnmount")
// 清除掉初始化时设置的监听事件
window.onresize = null
// 清除掉定时器
clearInterval(this.timer)
}
}
4. 新生命周期
先看整体流程图
注意react16自上而下对生命周期做了另一种解读,render阶段,pre-commit阶段还有commit阶段
4.1 老生命周期的问题
(1) componentWillMount
在ssr中这个方法将会被多次调用,所以会重复触发多遍,同时在这里如果绑定事件将无法解绑,导致内存泄漏,变得不够安全高效逐步废弃。(还有上面提到的fiber算法导致低优先级的原因)
(2) componentWillReceiveProps
- 外部组件多次频繁更新传入多次不同的 props,会导致不必要的异步请求
- 旧的React中componentWillReceiveProps方法是用来判断前后两个props是否相同,如果不同,则将新的props更新到相应的state上去。在这个过程中我们实际上是可以访问到当前props的,这样我们可能会对this.props做一些奇奇怪怪的操作,很可能会破坏state数据的单一数据源,导致组件状态变得不可预测。
(3) componetWillupdate
在React开启异步渲染模式后,在执行函数时读到的DOM元素状态并不总是渲染时相同,这就导致在componentDidUpdate中使用componentWillUpdate中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
4.2 getDerivedStateFromProps
第一次初始化组件以及后续的更新过程中(包括自身状态更新以及父传子),返回一个对象作为新的state,返回null则说明不需要在这里更新state。
对比componentWilReceiveProps的优点在于禁止了组件去访问this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去访问this.props并做其他一些让组件自身状态变得更加不可预测的事情。
其次,它将所有的异步操作合并在一起返回一次state,避免了频繁传入props导致的不断异步请求引起的资源浪费。
下面来看看具体用法,体现出该函数在初始化时和后续更新时会被调用,可以通过nextState获取新状态,有两个注意点:
export default class App extends Component {
state = {
myname: 'yoimiya'
}
// // (1)需要写成静态方法(要注意静态方法里没有自己的this)
static getDerivedStateFromProps(nextProps , nextState) {
console.log(nextState) //初始为{myname:'yoimiya} 点击后为{myname:'eula}
// (2)需要返回一个值作为新的state 可以为空 代表状态不变
return {
myname: nextState.myname.substring(0,1).toUpperCase() + nextState.myname.substring(1)
}
}
render() {
return (
<div>
<button onClick={() => {
this.setState({
myname: 'eula'
})
}}>click</button>
{this.state.myname}
</div>
)
}
}}
在实际运用中,建议该生命周期只负责将传过来的props转化为自己的state,需要搭配componentDidUpdate()来执行。
具体代码看自己vscode里的文件吧('./03_lifecycle/37_gdsfpDemo') 太长了,其实就是把componentWillReceiveProps替换成getDerivedStateFromProps + componentDidUpdate。
4.3 getSnapshotBeforeUpdate
取代了componetWillUpdate,解决了willUpdate和didUpdate之间时间间隔过长的问题。
触发时间为update发生的时候,在render之后didUpdate之前返回一个值,作为componentDidUpdate的第三个参数。
来看基本用法
export default class App extends Component {
state = {
text: 111
}
render() {
console.log('render')
return (
<div>
<button onClick={() =>{
this.setState({
text: 222
})
}}>click</button>
{this.state.text}
</div>
)
}
getSnapshotBeforeUpdate(){
console.log('snapshot');
// 必须得return点东西 作为参数传给DidUpdate
return 100;
}
componentDidUpdate(prevProps , prevState , value){
console.log('did update' , value)
}
//render
//render
//snapshot
//did update 100
}
来看个具体的案例,邮箱来邮件之后让视窗仍然保持在当前在看的邮件,而非自动弹到最新邮件处。
export default class App extends Component {
state = {
list: [1, 2, 3, 4, 5]
}
myRef = React.createRef();
render() {
return (
<div>
<h1>邮箱Demo</h1>
<div style={{ height: '200px', overflow: 'auto' }} ref={this.myRef}>
<ul>
{
this.state.list.map(item =>
<li key={item} style={{ height: '100px', background: 'lightgreen' }}>{item}</li>
)
}
</ul>
</div>
<button onClick={() => {
this.setState({
list: [...[11, 12, 13, 14, 15], ...this.state.list]
})
}}>收邮件</button>
</div>
)
}
getSnapshotBeforeUpdate() {
//获取未更新dom前容器高度
return this.myRef.current.scrollHeight
}
componentDidUpdate(prevProps , prevState , value){
// 将top值设为滚动高度差 成功实现“快照保存”邮件来前位置效果
this.myRef.current.scrollTop += this.myRef.current.scrollHeight - value;
}
}
5.PureComponent
顺带一提这个性能优化的问题,之前的shouldComponentUpdate需要自己去判断状态,属性等来决定组件的更新。
通过继承PureComponent可以通过一种叫shallowEqual的方法直接实现自动比较,也就是react帮你把活干了。
该方法可以应用于绝大部分场景,除非state或props一直会改变(比如倒计时),那么pureComponent就显得没有必要去比较新老状态是否改变了,反而会因为shallowEqual影响性能。
使用方法是直接继承即可
// 注意这里的导入
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
state = {
myname: 'yoimiya',
}
render() {
return (
<div>
<button onClick={() => {
this.setState({
myname: 'eula'
})
}}>click</button>
<span id='myname'>{this.state.myname}</span>
</div>
)
}
// this.state 老的状态
// nextState 新的状态
// shouldComponentUpdate(nextProps, nextState) {
// if (JSON.stringify(this.state) !== JSON.stringify(nextState)) {
// return true;
// } else {
// return false;
// }
// }
}
使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。
pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。