React中的组件生命周期

前言

没有坚固的底盘,如何构建高楼大厦。就像深圳华强北赛格大厦倾斜但是倒不了一样,随风飘扬。

今天主要是讲一下React的生命周期

简直就是底盘的底盘,生命都没有了,哪来的生存。

下面我们来看一下简单的例子:

import React, { Component } from "react";

class Index1 extends Component {
  state = {
    opacity: 1,
  };
  timer: number | undefined;
  componentDidMount() {
    console.log("componentDidMount");
    this.timer = setInterval(() => {
      let { opacity } = this.state;
      opacity -= 0.1;
      if (opacity <= 0) opacity = 1;
      this.setState({ opacity });
    }, 200);
  }

  //组件将要卸载
  componentWillUnmount(){
    console.log("componentWillUnmount")
    //清除定时器
    clearInterval(this.timer)
  }

  render(): React.ReactNode {
    return (
      <>
        <h2 style={{ opacity: this.state.opacity }}>李嘉欣来你家了</h2>
      </>
    );
  }
}

export default class example12 extends Component {
  state = {
    status: true,
  };
  changeIndex = () => {
    this.setState({ status: !this.state.status });
  };
  render() {
    return (
      <>
        <h2>生命周期</h2>
        {this.state.status ? <Index1></Index1> : ""}

        <button onClick={this.changeIndex}>李嘉欣不见了</button>
      </>
    );
  }
}
复制代码

效果图:

生命周期1.gif

从上面我们可以看出个大概:组件渲染时会执行componentDidMount,组件卸载时会执行componentWillUnmount

哪还有其他的生命周期吗?有的。

生命周期(旧)

在React 16以前的生命周期图是这样的:

2_react生命周期(旧).png

用一个简单的例子来看一下:

import React, { Component } from "react";

class Index1 extends Component<{ death: () => void; carName: string }, { count: number }> {
  constructor(props: { death: () => void; carName: string } | Readonly<{ death: () => void; carName: string }>) {
    console.log("Count---constructor");
    super(props);
    //初始化状态
    this.state = { count: 0 };
  }
  add = () => {
    //获取原状态
    const { count } = this.state;
    //更新状态
    this.setState({ count: count + 1 });
  };
  force = () => {
    this.forceUpdate();
  };
  //组件将要挂载的钩子
  componentWillMount() {
    console.log("Count---componentWillMount");
  }

  //组件将要挂载完毕的钩子
  componentDidMount() {
    console.log("Count---componentDidMount");
  }

  //组件将要卸载的钩子
  componentWillUnmount() {
    console.log("Count---componentWillUnmount");
  }

  //控制组件更细的阀门
  shouldComponentUpdate() {
    console.log("Count---shouldComponentUpdate");
    return true;
  }

  //组件将要更新的钩子
  componentWillUpdate() {
    console.log("Count---componentWillUpdate");
  }

  //组件更新完毕的钩子
  componentDidUpdate() {
    console.log("Count --- componentDidUpdate");
  }
  render(): React.ReactNode {
    console.log("Count---render");
    const { death, carName } = this.props;

    const { count } = this.state;
    return (
      <>
        <h3>当前求和为:{count}</h3>
        <h3>我现在开:{carName}</h3>
        <button onClick={this.add}>点我+1</button>
        <button onClick={() => death()}>卸载组件</button>
        <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
      </>
    );
  }
}

export default class example12 extends Component {
  state = {
    status: true,
    carName: "奔驰",
  };
  changeIndex = () => {
    this.setState({ status: !this.state.status });
  };
  changCar = () => {
    this.setState({ carName: "奥拓" });
  };
  render() {
    console.log("父组件的render");
    return (
      <>
        <h2>生命周期</h2>
        {this.state.status ? <Index1 death={this.changeIndex} carName={this.state.carName}></Index1> : ""}

        <hr />
        <div>
          <button onClick={this.changCar}>换车</button>
        </div>
      </>
    );
  }
}
复制代码

效果图:

生命周期旧.gif

可以从上面总结出以下知识点:

  1. 初始化阶段: 由父级render()触发---初次渲染
    • constructor()
    • componentWillMount()
    • render()
    • omponentDidMount() =====> 常用,一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
  2. 更新阶段: 由组件内部this.setSate()或父组件render触发
    • shouldComponentUpdate()
    • componentWillUpdate()
    • render() =====> 必须使用的一个
    • componentDidUpdate()
  3. 卸载组件: 由父组件的changeIndex事件触发
    • componentWillUnmount() =====> 常用,一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息

好奇的伙伴们就会发现右侧有一些警告:

image.png

对于这个官方是这样解释的:

image.png

大概的意思就是React准备启用这几个Api了,如果你需要使用的话,请在前面的加上前缀UNSAFE_

但是改了之后还是会报警告:

image.png

这个的大概意思就是,严格模式下使用UNSAFE_componentWillMount,咦我们什么时候开启了严格模式了?其实是用vite创建项目的使用自动给我们加上了:

image.png 只要把React.StrictMode注释就可以了。更多可以去React对这块的解释

看完旧的让我们看一下新的

生命周期(新)

1、生命周期有哪些?

React 16以上官网给的生命周期是这样的:

image.png

根据上面的图片归纳一下:

将组件生命周期分为三个阶段:

  • 装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
  • 更新过程(Update),组件状态发生变化,重新更新渲染的过程;
  • 卸载过程(Unmount),组件从DOM树中被移除的过程;

1)组件挂载阶段

挂载阶段组件被创建,然后组件实例插入到 DOM 中,完成组件的第一次渲染,该过程只会发生一次,在此阶段会依次调用以下这些方法:

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount
(1)constructor

组件的构造函数,第一个被执行,若没有显式定义它,会有一个默认的构造函数,但是若显式定义了构造函数,我们必须在构造函数中执行 super(props),否则无法在构造函数中拿到this。

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数Constructor

constructor中通常只做两件事:

  • 初始化组件的 state
  • 给事件处理方法绑定 this,一般都是箭头函数代替了
constructor(props: { death: () => void; carName: string } | Readonly<{ death: () => void; carName: string }>) {
  console.log("Count---constructor");
  super(props);
  //初始化状态
  this.state = { count: 0 };
  this.add = this.add.bind(this);
}
复制代码
(2)getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
复制代码

这是个静态方法,所以不能在这个函数里使用 this,有两个参数 propsstate,分别指接收到的新参数和当前组件的 state 对象,这个函数会返回一个对象用来更新当前的 state 对象,如果不需要更新可以返回 null

该函数会在装载时,接收到新的 props 或者调用了 setStateforceUpdate 时被调用。如当接收到新的属性想修改 state ,就可以使用。

import React, { Component } from "react";

class Index1 extends Component<{ count: number }> {
  state = {
    count: 0,
  };
  static getDerivedStateFromProps(props, state) {
    if (props.count !== state.count) {
      return {
        count: props.count,
      };
    }
    return null;
  }
  add = () => {
    this.setState({
      count: this.state.count + 1,
    });
  };
  render() {
    return (
      <>
        <h1>现在的计算的Count:{this.state.count}</h1>
        <button onClick={this.add}>点我+1</button>
      </>
    );
  }
}

export default class example14 extends Component {
  render() {
    return <Index1 count={10}></Index1>;
  }
}
复制代码

现在可以显式传入 count ,但是这里有个问题,如果想要通过点击实现 state.count 的增加,但这时会发现值不会发生任何变化,一直保持 props 传进来的值。这是由于在 React 16.4^ 的版本中 setStateforceUpdate 也会触发这个生命周期,所以当组件内部 state 变化后,就会重新走这个方法,同时会把 state 值赋值为 props 的值。因此需要多加一个字段来记录之前的 props 值,这样就会解决上述问题。具体如下:

......其他都一样,省略了
state = {
  count: 0,
  preCount: 0,
};
static getDerivedStateFromProps(props, state) {
  if (props.count !== state.preCount) {
    return {
      count: props.count,
      preCount: props.count,
    };
  }
  return null;
}
......其他都一样,省略了
复制代码
(3)render

render是React 中最核心的方法,一个组件中必须要有这个方法,它会根据状态 state 和属性 props 渲染组件。这个函数只做一件事,就是返回需要渲染的内容,所以不要在这个函数内做其他业务逻辑,通常调用该方法会返回以下类型中一个:

  • React 元素:这里包括原生的 DOM 以及 React 组件;
  • 数组和 Fragment(片段) :可以返回多个元素;
  • Portals(插槽) :可以将子元素渲染到不同的 DOM 子树种;
  • 字符串和数字:被渲染成 DOM 中的 text 节点;
  • 布尔值或 null:不渲染任何内容。
(4)componentDidMount()

componentDidMount()会在组件挂载后(插入 DOM 树中)立即调。该阶段通常进行以下操作:

  • 执行依赖于DOM的操作;
  • 发送网络请求;(官方建议)
  • 添加订阅消息(会在componentWillUnmount取消订阅);

如果在 componentDidMount 中调用 setState ,就会触发一次额外的渲染,多调用了一次 render 函数,由于它是在浏览器刷新屏幕前执行的,所以用户对此是没有感知的,但是我应当避免这样使用,这样会带来一定的性能问题,尽量是在 constructor 中初始化 state 对象。

在组件装载之后,将计数数字变为1:

......其他都一样,省略了
componentDidMount () {
  this.setState({
    count: 1
  })
}
......其他都一样,省略了
复制代码

2)组件更新阶段

当组件的 props 改变了,或组件内部调用了 setState/forceUpdate,会触发更新重新渲染,这个过程可能会发生多次。这个阶段会依次调用下面这些方法:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate
(1)shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
复制代码

在说这个生命周期函数之前,来看两个问题:

  • setState 函数在任何情况下都会导致组件重新渲染吗?例如下面这种情况:
this.setState({number: this.state.number})
复制代码
  • 如果没有调用 setState,props 值也没有变化,是不是组件就不会重新渲染?

第一个问题答案是 ,第二个问题如果是父组件重新渲染时,不管传入的 props 有没有变化,都会引起子组件的重新渲染。

那么有没有什么方法解决在这两个场景下不让组件重新渲染进而提升性能呢?这个时候 shouldComponentUpdate 登场了,这个生命周期函数是用来提升速度的,它是在重新渲染组件开始前触发的,默认返回 true,可以比较 this.propsnextPropsthis.statenextState 值是否变化,来确认返回 true 或者 false。当返回 false 时,组件的更新过程停止,后续的 rendercomponentDidUpdate 也不会被调用。

注意: 添加 shouldComponentUpdate 方法时,不建议使用深度相等检查(如使用 JSON.stringify()),因为深比较效率很低,可能会比重新渲染组件效率还低。而且该方法维护比较困难,建议使用该方法会产生明显的性能提升时使用。

(2)getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
复制代码

这个方法在 render 之后,componentDidUpdate 之前调用,有两个参数 prevPropsprevState,表示更新之前的 propsstate,这个函数必须要和 componentDidUpdate 一起使用,并且要有一个返回值,默认是 null,这个返回值作为第三个参数传给 componentDidUpdate

(3)componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。 该阶段通常进行以下操作:

  • 当组件更新后,对 DOM 进行操作;
  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
componentDidUpdate(prevProps, prevState, snapshot){}
复制代码

该方法有三个参数:

  • prevProps: 更新前的props
  • prevState: 更新前的state
  • snapshot: getSnapshotBeforeUpdate()生命周期的返回值

3)组件卸载阶段

卸载阶段只有一个生命周期函数,componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作:

  • 清除 timer,取消网络请求或清除
  • 取消在 componentDidMount() 中创建的订阅等;

这个生命周期在一个组件被卸载和销毁之前被调用,因此你不应该再这个方法中使用 setState,因为组件一旦被卸载,就不会再装载,也就不会重新渲染。

4)错误处理阶段

componentDidCatch(error, info),此生命周期在后代组件抛出错误后被调用。 它接收两个参数∶

  • error:抛出的错误。
  • info:带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息

React常见的生命周期如下: image.png React常见生命周期的过程大致如下:

  • 挂载阶段,首先执行constructor构造方法,来创建组件
  • 创建完成之后,就会执行render方法,该方法会返回需要渲染的内容
  • 随后,React会将需要渲染的内容挂载到DOM树上
  • 挂载完成之后就会执行componentDidMount生命周期函数
  • 如果我们给组件创建一个props(用于组件通信)、调用setState(更改state中的数据)、调用forceUpdate(强制更新组件)时,都会重新调用render函数
  • render函数重新执行之后,就会重新进行DOM树的挂载
  • 挂载完成之后就会执行componentDidUpdate生命周期函数
  • 当移除组件时,就会执行componentWillUnmount生命周期函数

React主要生命周期总结:

  1. getDefaultProps:这个函数会在组件创建之前被调用一次(有且仅有一次),它被用来初始化组件的 Props;
  2. getInitialState:用于初始化组件的 state 值;
  3. componentWillMount:在组件创建后、render 之前,会走到 componentWillMount 阶段。这个阶段我个人一直没用过、非常鸡肋。后来React 官方已经不推荐大家在 componentWillMount 里做任何事情、到现在 React16 直接废弃了这个生命周期,足见其鸡肋程度了;
  4. render:这是所有生命周期中唯一一个你必须要实现的方法。一般来说需要返回一个 jsx 元素,这时 React 会根据 props 和 state 来把组件渲染到界面上;不过有时,你可能不想渲染任何东西,这种情况下让它返回 null 或者 false 即可;
  5. componentDidMount:会在组件挂载后(插入 DOM 树中后)立即调用,标志着组件挂载完成。一些操作如果依赖获取到 DOM 节点信息,我们就会放在这个阶段来做。此外,这还是 React 官方推荐的发起 ajax 请求的时机。该方法和 componentWillMount 一样,有且仅有一次调用。

2. React 废弃了哪些生命周期?为什么?

被废弃的三个函数都是在render之前,因为fber的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次。另外的一个原因则是,React想约束使用者,好的框架能够让人不得已写出容易维护和扩展的代码,这一点又是从何谈起,可以从新增加以及即将废弃的生命周期分析入手

1) componentWillMount

首先这个函数的功能完全可以使用componentDidMount和 constructor来代替,异步获取的数据的情况上面已经说明了,而如果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在constructor中执行,除此之外,如果在 willMount 中订阅事件,但在服务端这并不会执行 willUnMount事件,也就是说服务端会导致内存泄漏所以componentWilIMount完全可以不使用,但使用者有时候难免因为各 种各样的情况在 componentWilMount中做一些操作,那么React为了约束开发者,干脆就抛掉了这个API

2) componentWillReceiveProps

在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWilReceiveProps 中判断前后两个 props 是否相同,如果不同再将新的 props更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的时,根据传入的某个值,直接定位到某个 Tab。为了解决这些问题,React引入了第一个新的生命周期:getDerivedStateFromProps。它有以下的优点∶

  • getDSFP是静态方法,在这里不能使用this,也就是一个纯函数,开发者不能写出副作用的代码
  • 开发者只能通过prevState而不是prevProps来做对比,保证了state和props之间的简单关系以及不需要处理第一次渲染时prevProps为空的情况
  • 基于第一点,将状态变化(setState)和昂贵操作(tabChange)区分开,更加便于 render 和 commit 阶段操作或者说优化。

3) componentWillUpdate

与 componentWillReceiveProps 类似,许多开发者也会在 componentWillUpdate 中根据 props 的变化去触发一些回调 。 但不论是 componentWilReceiveProps 还 是 componentWilUpdate,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount 类 似, componentDidUpdate 也不存在这样的问题,一次更新中 componentDidUpdate 只会被调用一次,所以将原先写在 componentWillUpdate 中 的 回 调 迁 移 至 componentDidUpdate 就可以解决这个问题。

另外一种情况则是需要获取DOM元素状态,但是由于在fber中,render可打断,可能在wilMount中获取到的元素状态很可能与实际需要的不同,这个通常可以使用第二个新增的生命函数的解决 getSnapshotBeforeUpdate(prevProps, prevState)

4) getSnapshotBeforeUpdate(prevProps, prevState)

返回的值作为componentDidUpdate的第三个参数。与willMount不同的是,getSnapshotBeforeUpdate会在最终确定的render执行之前执行,也就是能保证其获取到的元素状态与didUpdate中获取到的元素状态相同。参考代码:

//无论新增多少条新闻,保持滚动条的位置
import React, { Component } from "react";
import "./example16.css"
class NewsList extends Component {
  list = React.createRef<HTMLDivElement>();

  state = { newsArr: [] };

  componentDidMount() {
    setInterval(() => {
      //获取原状态
      const { newsArr } = this.state;
      //模拟一条新闻
      const news = "新闻" + (newsArr.length + 1);
      //更新状态
      this.setState({ newsArr: [news, ...newsArr] });
    }, 1000);
  }

  getSnapshotBeforeUpdate() {
    return this.list.current.scrollHeight;
  }

  componentDidUpdate(preProps, preState, height) {
    this.list.current.scrollTop += this.list.current.scrollHeight - height;
  }

  render() {
    return (
      <div className="list" ref={this.list}>
        {this.state.newsArr.map((n, index) => {
          return (
            <div key={index} className="news">
              {n}
            </div>
          );
        })}
      </div>
    );
  }
}

export default class example16 extends Component {
  render() {
    return (
      <>
        <NewsList></NewsList>
      </>
    );
  }
}
复制代码

效果图:

保持滚动条位置.gif

总结

React的生命周期就介绍到这里了,只要是想从就得开始引入,慢慢的去学习新的。这样好入手一些。

猜你喜欢

转载自juejin.im/post/7070059548857335845