react-生命周期

loading 2022年12月06日 183次浏览

先看旧生命周期的整体流程图

首先要注意,只有类组件有生命周期,函数式组件只能通过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中。

  1. export default class App extends Component {
  2. state = {
  3. list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
  4. }

  5. UNSAFE_componentWillMount() {
  6. console.log(document.querySelectorAll('li'))//获取到一个空数组
  7. }

  8. render() {
  9. return (
  10. <div>
  11. <div id='wrapper' style={{height:'150px',background:'lightgreen',overflow:'hidden'}}>
  12. <ul>
  13. {
  14. this.state.list.map(item =>
  15. <li key={item}>{item}</li>
  16. )
  17. }
  18. </ul>
  19. </div>
  20. </div>
  21. )
  22. }

  23. componentDidMount() {
  24. console.log(document.querySelectorAll('li'))//获取到10个li

  25. // 要在这里new 因为此时已经获取到dom节点
  26. new BetterScroll('#wrapper')
  27. }
  28. }

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的第二个参数中作为回调函数执行。

  1. componentDidMount() {
  2. axios.get(`/test.json`).then(res => {
  3. this.setState({
  4. filmList: res.data.data.films
  5. }, () => {
  6. new BetterScroll('#wrapper')
  7. })
  8. })
  9. }

可以不在setState中写,直接在axios获取到数据并更新状态后的componentDidUpdate中写,效果相同。

componentDidUpdate也有对应的缺点,就是会执行多次,每次更新状态都会使得它重新执行。

  1. componentDidMount() {
  2. axios.get(`/test.json`).then(res => {
  3. this.setState({
  4. filmList: res.data.data.films
  5. })
  6. })
  7. }

  8. componentDidUpdate() {
  9. new BetterScroll('#wrapper')
  10. }

该生命周期可以传入两个参数,用于记录老的属性和老的状态

  1. componentDidUpdate(prevProps , prevState) {
  2. console.log(prevState.filmList) //空数组[]
  3. if(prevState.filmList.length === 0){
  4. new BetterScroll('#wrapper')
  5. }
  6. }

2.4 shouldComponentUpdate

如果该方法返回false,那么就会阻止render调用

举个例子,第一次点击按钮后myname会改变,但是之后每次点击按钮,虽然state值没有改变,但还是会继续触发虚拟dom和真实dom的对比,render重新渲染页面。

  1. export default class App extends Component {
  2. state = {
  3. myname: 'yoimiya',
  4. }
  5. render() {
  6. return (
  7. <div>
  8. <button onClick={() => {
  9. this.setState({
  10. myname: 'eula'
  11. })
  12. }}>click</button>

  13. <span id='myname'>{this.state.myname}</span>
  14. </div>
  15. )
  16. }
  17. }

该生命周期也是可以传两个参数,分别代表新属性和新状态

  1. // this.state 老的状态
  2. // nextState 新的状态
  3. shouldComponentUpdate(nextProps , nextState){
  4. if(this.state.myname !== nextState.myname){
  5. // 允许render调用
  6. return true;
  7. }else{
  8. // 不允许render调用
  9. return false;
  10. }
  11. }

为了不一一对比对象中的属性,直接将对象序列化后对比两个整体对象。

  1. // this.state 老的状态
  2. // nextState 新的状态
  3. shouldComponentUpdate(nextProps , nextState){
  4. if(JSON.stringify(this.state) !== JSON.stringify(nextState)){
  5. return true;
  6. }else{
  7. return false;
  8. }
  9. }

该生命周期对于性能优化有着重要的作用,举个例子:一个父组件底下10个子组件,根据父组件状态修改其中1个子组件会导致剩下的也要重新渲染,大大影响性能。

先来看没用shouldComponentUpdate优化的代码,根据输入框的value修改current状态,将current和index传给子组件做对比,相等时背景色改变:

  1. class Box extends Component {
  2. render() {
  3. return (
  4. <div style={{
  5. width: '100px', height: '100px', border: '1px solid grey', margin: '10px', float: 'left',
  6. // 根据条件来决定背景色
  7. background: this.props.current === this.props.index ? 'lightgreen' : ''
  8. }}>

  9. </div>
  10. )
  11. }
  12. }

  13. export default class App extends Component {
  14. state = {
  15. list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
  16. current: 0
  17. }
  18. render() {
  19. return (
  20. <div>
  21. {/* 根据输入框内容修改current 记得用Number()转换 */}
  22. <input type='number' onChange={(e) =>{
  23. this.setState({
  24. current : Number(e.target.value)
  25. })
  26. }}></input>

  27. <div style={{ overflow: 'hidden' }}>
  28. {
  29. this.state.list.map((item, index) =>
  30. // 将current和index传给子组件
  31. <Box key={item} current={this.state.current} index={index}></Box>
  32. )
  33. }
  34. </div>
  35. </div>
  36. )
  37. }
  38. }

但是这份代码有个问题,每次修改current,10个Box都会被render重新渲染一次,造成性能影响。

因此可以在Box组件里加入这段代码,实现了每次render只更新2个(老的和新的)需要被更新的Box:

  1. shouldComponentUpdate(newProps){
  2. // 只允许render更新老的Box:选中->未选中,和新的Box:未选中->选中
  3. if(this.props.current === this.props.index || newProps.current === newProps.index){
  4. return true;
  5. }
  6. return false;
  7. }

2.5 componentWillReceiveProps

此生命周期也已经被弃用

父组件修改属性时触发,因此应该写在子组件内。

但是其实并不需要将属性传给孩子,哪怕只是在父组件中更新状态,也会导致子组件的生命周期更新,也会触发该方法。

这个生命周期唯一作用是最先获得父组件传来的属性,可以利用属性进行相应的操作

通过形参中的nextProps(名字不固定),来获取最新的属性

比如这个例子

  1. class Child extends Component {
  2. render() {
  3. return (
  4. <div>
  5. child
  6. </div>
  7. )
  8. }
  9. componentWillReceiveProps(nextProps){
  10. console.log(this.props.text , nextProps.text)
  11. // 111 222
  12. }
  13. }

  14. export default class App extends Component {
  15. state = {
  16. text : '111'
  17. }
  18. render() {
  19. return (
  20. <div>
  21. {
  22. this.state.text
  23. }
  24. <button onClick={() =>{
  25. this.setState({
  26. text : '222'
  27. })
  28. }}>click</button>
  29. <Child text={this.state.text}></Child>
  30. </div>
  31. )
  32. }
  33. }

3. 销毁阶段

销毁阶段就一个函数:componentWillUnmount

作用是在移除组件之前进行清理操作,比如计时器和事件监听器。

  1. export default class App extends Component {
  2. state = {
  3. isCreated: true
  4. }
  5. render() {
  6. return (
  7. <div>
  8. <button onClick={() => {
  9. this.setState({
  10. isCreated: !this.state.isCreated
  11. })
  12. }}>click</button>
  13. {this.state.isCreated && <Child />}
  14. </div>
  15. )
  16. }
  17. }

  18. class Child extends Component {
  19. render() {
  20. return <div>
  21. child
  22. </div>
  23. }

  24. componentDidMount() {
  25. window.onresize = () => {
  26. console.log("resize")
  27. }

  28. // this是Child组件的一个实例
  29. this.timer = setInterval(() => {
  30. console.log("111")
  31. }, 1000)
  32. }


  33. componentWillUnmount() {
  34. console.log("componentWillUnmount")

  35. // 清除掉初始化时设置的监听事件
  36. window.onresize = null

  37. // 清除掉定时器
  38. clearInterval(this.timer)
  39. }
  40. }

4. 新生命周期

先看整体流程图

注意react16自上而下对生命周期做了另一种解读,render阶段,pre-commit阶段还有commit阶段

4.1 老生命周期的问题

(1) componentWillMount
在ssr中这个方法将会被多次调用,所以会重复触发多遍,同时在这里如果绑定事件将无法解绑,导致内存泄漏,变得不够安全高效逐步废弃。(还有上面提到的fiber算法导致低优先级的原因)

(2) componentWillReceiveProps

  1. 外部组件多次频繁更新传入多次不同的 props,会导致不必要的异步请求
  2. 旧的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获取新状态,有两个注意点:

  1. export default class App extends Component {
  2. state = {
  3. myname: 'yoimiya'
  4. }
  5. // // (1)需要写成静态方法(要注意静态方法里没有自己的this)
  6. static getDerivedStateFromProps(nextProps , nextState) {
  7. console.log(nextState) //初始为{myname:'yoimiya} 点击后为{myname:'eula}

  8. // (2)需要返回一个值作为新的state 可以为空 代表状态不变
  9. return {
  10. myname: nextState.myname.substring(0,1).toUpperCase() + nextState.myname.substring(1)
  11. }
  12. }
  13. render() {
  14. return (
  15. <div>
  16. <button onClick={() => {
  17. this.setState({
  18. myname: 'eula'
  19. })
  20. }}>click</button>
  21. {this.state.myname}
  22. </div>
  23. )
  24. }
  25. }}

在实际运用中,建议该生命周期只负责将传过来的props转化为自己的state,需要搭配componentDidUpdate()来执行。

具体代码看自己vscode里的文件吧('./03_lifecycle/37_gdsfpDemo') 太长了,其实就是把componentWillReceiveProps替换成getDerivedStateFromProps + componentDidUpdate

4.3 getSnapshotBeforeUpdate

取代了componetWillUpdate,解决了willUpdate和didUpdate之间时间间隔过长的问题。

触发时间为update发生的时候,在render之后didUpdate之前返回一个值,作为componentDidUpdate的第三个参数。

来看基本用法

  1. export default class App extends Component {
  2. state = {
  3. text: 111
  4. }
  5. render() {
  6. console.log('render')
  7. return (
  8. <div>
  9. <button onClick={() =>{
  10. this.setState({
  11. text: 222
  12. })
  13. }}>click</button>
  14. {this.state.text}
  15. </div>
  16. )
  17. }
  18. getSnapshotBeforeUpdate(){
  19. console.log('snapshot');
  20. // 必须得return点东西 作为参数传给DidUpdate
  21. return 100;
  22. }
  23. componentDidUpdate(prevProps , prevState , value){
  24. console.log('did update' , value)
  25. }

  26. //render
  27. //render
  28. //snapshot
  29. //did update 100
  30. }

来看个具体的案例,邮箱来邮件之后让视窗仍然保持在当前在看的邮件,而非自动弹到最新邮件处。

  1. export default class App extends Component {
  2. state = {
  3. list: [1, 2, 3, 4, 5]
  4. }
  5. myRef = React.createRef();
  6. render() {
  7. return (
  8. <div>
  9. <h1>邮箱Demo</h1>

  10. <div style={{ height: '200px', overflow: 'auto' }} ref={this.myRef}>
  11. <ul>
  12. {
  13. this.state.list.map(item =>
  14. <li key={item} style={{ height: '100px', background: 'lightgreen' }}>{item}</li>
  15. )
  16. }
  17. </ul>
  18. </div>

  19. <button onClick={() => {
  20. this.setState({
  21. list: [...[11, 12, 13, 14, 15], ...this.state.list]
  22. })
  23. }}>收邮件</button>
  24. </div>
  25. )
  26. }
  27. getSnapshotBeforeUpdate() {
  28. //获取未更新dom前容器高度
  29. return this.myRef.current.scrollHeight
  30. }
  31. componentDidUpdate(prevProps , prevState , value){
  32. // 将top值设为滚动高度差 成功实现“快照保存”邮件来前位置效果
  33. this.myRef.current.scrollTop += this.myRef.current.scrollHeight - value;
  34. }
  35. }

5.PureComponent

顺带一提这个性能优化的问题,之前的shouldComponentUpdate需要自己去判断状态,属性等来决定组件的更新。

通过继承PureComponent可以通过一种叫shallowEqual的方法直接实现自动比较,也就是react帮你把活干了。

该方法可以应用于绝大部分场景,除非state或props一直会改变(比如倒计时),那么pureComponent就显得没有必要去比较新老状态是否改变了,反而会因为shallowEqual影响性能。

使用方法是直接继承即可

  1. // 注意这里的导入
  2. import React, { PureComponent } from 'react'

  3. export default class App extends PureComponent {
  4. state = {
  5. myname: 'yoimiya',
  6. }
  7. render() {
  8. return (
  9. <div>
  10. <button onClick={() => {
  11. this.setState({
  12. myname: 'eula'
  13. })
  14. }}>click</button>

  15. <span id='myname'>{this.state.myname}</span>
  16. </div>
  17. )
  18. }

  19. // this.state 老的状态
  20. // nextState 新的状态
  21. // shouldComponentUpdate(nextProps, nextState) {
  22. // if (JSON.stringify(this.state) !== JSON.stringify(nextState)) {
  23. // return true;
  24. // } else {
  25. // return false;
  26. // }
  27. // }
  28. }

使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。

pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。