【React】深入理解React组件生命周期----图文详解(含代码)

文章目录

  • 组件的生命周期
    • 1️⃣ 理解
    • 2️⃣ 案例
    • 3️⃣生命周期流程图(旧)
      • A. 实现setState 和 forceUpdate 流程
      • B. 实现组件之间传值流程
    • 4️⃣ 对比新旧生命周期
    • 5️⃣ 生命周期流程图(新)
      • A. getDerivedStateFromProps(几乎用不到,使用场景及其罕见)
      • B. getSnapshotBeforeUpdate
      • C.getSnapshotBeforeUpdate使用场景
    • 6️⃣ 重要的三个钩子
    • 7️⃣ 即将废弃的钩子

组件的生命周期

先来看个例子引出生命周期

1️⃣ 理解

  1. 组件从创建到死亡它会经历一些特定的阶段。
  2. React组件中包含一系列钩子函数(生命周期回调函数), 会在特定的时刻调用。
  3. 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。

2️⃣ 案例

需求:定义组件实现以下功能:

  1. 让指定的文本做显示/隐藏的渐变动画
  2. 从完全可见,到彻底消失,耗时2S
  3. 点击”不活了“按钮从界面中卸载组件

效果如下:

【React】深入理解React组件生命周期----图文详解(含代码)_第1张图片

具体代码实现:

<script type="text/babel">
    // 创建组件
    // 生命周期回调函数 <=> 生命周期钩子函数(我定义的,react帮我调用的)
    class Life extends React.Component {
      //  opacity属性设置一个div元素的透明度级别,默认值为1(完全不透明)
      state = { opacity: 1 }

      death = () => {
        // 清除定时器
        // clearInterval(this.timer)
        // 卸载组件
        // unmountComponentAtNode:卸载组件在哪个节点里
     ReactDOM.unmountComponentAtNode(document.getElementById('test'))
      }

      // 组件挂载完毕之后调用
      componentDidMount() {
        // 定时器是挂载在window上的
        // 当组件卸载完之后,定时器也不会消失
        this.timer = setInterval(() => {
          // 获取原状态
          let { opacity } = this.state
          // 减小0.1
          opacity -= 0.1
          if (opacity <= 0) opacity = 1
          // 更新状态,设置新的透明度
          this.setState({ opacity: opacity })
        }, 200)
      }

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

      //render调用的时机:初始化渲染,状态更新之后 
      render() {
        console.log('render')
        // render被调1+n次(1是初始化显示,n是更改状态的次数)
        // 每次更新state状态都会调用一次render并新建一个计时器
        // 设置一个定时器,让h2标签里的字体透明度从1到0再到1一直循环下去
        // setInterval(() => {
        //   获取原状态
        //   let { opacity } = this.state
        //   减小0.1
        //   opacity -= 0.1
        //   if (opacity <= 0) opacity = 1
        //   更新状态,设置新的透明度
        //   this.setState({ opacity: opacity })
        // }, 200)
        {/*
          为什么不能将计时器放在render中??
          如果setInterval计时器放在render中,该计时器内部又封装了this.setState方法,
          第一次函数挂载到页面中,调用setInterval函数,函数每过200ms就会对state中的数据进行更改,
          每次更改后又会调用render对组件更新重新渲染,重新渲染时又触发了serInterval函数,
          setInterval函数一直在被调用,而之前的计时器又没有被关闭,页面在不断的生成计时器,
          导致CPU不断上升,所以计时器不能放在render中。
        */}
        return (
          <div>
            <h2 style={{ opacity: this.state.opacity }}>React学不会怎么办?</h2>
            <button onClick={this.death}>不活了</button>
          </div>
        )
      }
    }
    // 渲染组件(把组件挂载(mount)到页面上)
    ReactDOM.render(<Life />, document.getElementById('test'))
  </script>

3️⃣生命周期流程图(旧)

【React】深入理解React组件生命周期----图文详解(含代码)_第2张图片

A. 实现setState 和 forceUpdate 流程

案例:

需求:

  1. 点击+1 按钮,求和数自动+1;
  2. 点击卸载组件,组件从页面中卸载;
  3. 点击不更改任何状态中的数据,强制更新一下,不更改状态中的数据,组件强制更新;

效果如下:

【React】深入理解React组件生命周期----图文详解(含代码)_第3张图片

代码如下:

<script type="text/babel">
    // setState 和 forceUpdate 流程
    // 创建组件
    class Count extends React.Component {
      // 构造器
      constructor(props) {
        console.log('Count---constructor')
        super(props)
        // 初始化状态
        this.state = { count: 0 }
      }

      // 加1按钮的回调
      add = () => {
        // 获取原状态
        const { count } = this.state
        // 更新状态
        this.setState({ count: count + 1 })
      }

      // 卸载组件按钮的回调
      death = () => {
        ReactDOM.unmountComponentAtNode(document.getElementById('test'))
      }

      // 强制更新按钮的回调
      force = () => {
        this.forceUpdate() //通过forceUpdate方法可以做到即使不使用state属性也可以更新界面
      }

      // 初始化阶段
      // 组件将要挂载之前的钩子
      componentWillMount() {
        console.log('Count---componentWillMount')
      }

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

      // 更新阶段
      // 控制组件更新的“阀门” 。
      // 如果不写这个钩子,react底层会自动补一个默认返回值为true的shouldComponentUpdate(),表示可以更新,
      // 如果返回值为false,则不会执行后续的生命周期函数
      shouldComponentUpdate() {
        console.log('Count---shouldComponentUpdate')
        return true
      }

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

      // 组件更新完毕的钩子
      componentDidUpdate() {
        console.log('Count---componentDidUpdate')
      }
    
      // 卸载阶段
      // 组件将要卸载的钩子
      componentWillUnmount() {
        console.log('Count---componentWillUnmount')
      }

      render() {
        console.log('Count---render')
        const { count } = this.state
        return (
          <div>
            <h2>当前求和为:{count}</h2>
            <button onClick={this.add}>点我+1</button>
            <button onClick={this.death}>卸载组件</button>
            <button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
          </div>
        )
      }
    }

    // 渲染组件
    ReactDOM.render(< Count />, document.getElementById('test'))
  </script>
  • 组件挂载到页面时:

    constructor(当前类的构造函数) => componentWillMount(组件将要挂载之前的钩子) => render(渲染组件到页面) => componentDidMount (组件挂载完毕的钩子)

  • 组件更新页面时(setState()):

    shouldComponentUpdate(控制组件更新的“阀门”:如果返回值为false,则不会执行后续的生命周期函数)=> componentWillUpdate(组件将要更新的钩子)=> render(渲染组件到页面)=> componentDidUpdate(组件更新完毕的钩子)

  • 组件强制更新页面时(forceUpdate()):绕开了shouldComponentUpdate阀门,即使不更新state中的数据,shouldComponentUpdate的返回值为false,也会更新页面

    componentWillUpdate(组件将要更新的钩子)=> render(渲染组件到页面)=> componentDidUpdate(组件更新完毕的钩子)

  • 组件卸载:

    componentWillUnmount(组件将要卸载的钩子)


总结:

生命周期的三个阶段(旧)如下:

  • 初始化阶段:由ReactDOM.render()触发—页面初次渲染
    • constructor()
    • componentWillMount()
    • render()
    • componentDidMount() ====>常用(一般在这个钩子中做一些初始化的事,例如:开启定时器,发送网络请求,订阅消息)(出生那天)
  • 更新阶段:由组件内部this.setState()父组件重新render触发
    • shouldComponentUpdate()
    • componentWillUpdate()
    • render() ====>必须使用的一个
    • componentDidUpdate()
  • 卸载阶段:由ReactDOM.unmountComponentAtNode()触发
    • componentWillUnmount() ====>常用(一般在这个钩子中做一些收尾的事,例如:关闭定时器,取消订阅消息)(死亡那天)

B. 实现组件之间传值流程

案例:

需求:

  1. 点击换车按钮,显示车从”奔驰“ 变成 ”宝马“。

效果如下:

【React】深入理解React组件生命周期----图文详解(含代码)_第4张图片

代码如下:

<script type="text/babel">
    // 父组件render流程
    // 父组件A
    class A extends React.Component {
      // 初始化状态
      state = { carName: '奔驰' }

      changeCar = () => {
        this.setState({ carName: '宝马' })
      }

      render() {
        return (
          <div>
            <div>我是A组件</div>
            {/* 
              点击换车,调用了changeCar,更改了A组件里的状态,
              导致A组件调用了render,正是父组件重新render
            */}
            <button onClick={this.changeCar}>换车</button>
            {/*
            父组件A将state中的carName传给子组件B的props.carName中
            */}
            <B carName={this.state.carName} />
          </div>
        )
      }
    }

    // 子组件B
    class B extends React.Component {
      //组件将要接收新的props的钩子
      // 必须经过setStateA组件改变,然后调用render,这时B组件重新接收一个新的值,重新调用render
      componentWillReceiveProps(nextProps) {
        // this.props 表示旧的props中的数据
        // nextProps 表示传入新的props
        console.log('B---componentWillReceiveProps', nextProps)
      }

      render() {
        console.log('B---render')
        return (
          {/*
          子组件接收carName,从props中取出
          */}
          <div>我是B组件,接收到的车是:{this.props.carName}</div>
        )
      }
    }
    // 渲染组件
    ReactDOM.render(<A />, document.getElementById('test'))
  </script>

父组件render(父组件A传值给子组件B):

componentWillReceiveProps(组件将要接收新的props的钩子) => shouldComponentUpdate(控制组件更新的“阀门”) => componentWillUpdate(组件将要更新的钩子) => render(渲染) => componentDidUpdate(组件更新完毕的钩子)

componentWillReceiveProps:在第一次传入props时不调用,只有在props更新时才会执行,可以用此钩子监听子组件props的改变。

4️⃣ 对比新旧生命周期

【React】深入理解React组件生命周期----图文详解(含代码)_第5张图片

17.0:新的生命周期函数删除了componentWillMountcomponentWillReceivePropscomponentWillUpdate,增加了getDerivedStateFromPropsgetSnapshotBeforeUpdate

官方说明如下:
【React】深入理解React组件生命周期----图文详解(含代码)_第6张图片

5️⃣ 生命周期流程图(新)

【React】深入理解React组件生命周期----图文详解(含代码)_第7张图片

A. getDerivedStateFromProps(几乎用不到,使用场景及其罕见)

在写这个生命周期函数时,要在getDerivedStateFromProps加上static,否则会报错。

在这里插入图片描述

【React】深入理解React组件生命周期----图文详解(含代码)_第8张图片

原因是:在Count组件中,getDerivedStateFromProps生命周期钩子定义在了实例身上,将会被忽略,不能写成这个钩子是给实例用的,实例不能调用这个钩子。要定义成一个静态方法,加上一个关键字static。

加上static之后,页面打印出了getDerivedStateFromProps,但是页面又显示一个报错提示:

【React】深入理解React组件生命周期----图文详解(含代码)_第9张图片

原因是类Count自身上的getDerivedStateFromProps函数应该返回一个状态对象,或者null,必须要有返回值。但是却返回了undefined。

当返回值是一个状态对象时:

class Count extends React.Component {
      // 构造器
      constructor(props) {
        console.log('Count---constructor')
        super(props)
        // 初始化状态
        this.state = { count: 0 }
      }
  		// 从props中得到一个派生的状态
      // 若state 的值在任何时候都取决于 props,那么可以使用
      static getDerivedStateFromProps(props,state) {
        console.log('getDerivedStateFromProps', props,state)
       //return {count:108}
        return props
      }
}
  	// 渲染组件
    ReactDOM.render(< Count count='199' />, document.getElementById('test'))

【React】深入理解React组件生命周期----图文详解(含代码)_第10张图片

  • return {count:108}利用这个返回值可以指定一个状态,导致状态不能更改了,只要返回一个状态对象,对象里面包含的key,在初始状态里也有,那么就不以初始化的状态为主了,以返回的状态对象为主。

  • getDerivedStateFromProps()可以接收props参数。给Count组件传递props(< Count count=‘199’ />),然后getDerivedStateFromProps(props)可以接收到,返回props(return props),所以从props中得到一个派生的状态(不是自己写的),即 state 的值在任何时候都取决于 props,无论是初始化,或者修改都不起作用。

  • getDerivedStateFromProps()可以接收state参数,即初始化的状态对象。


言而总之,新的静态 getDerivedStateFromProps 生命周期方法在组件实例化之后以及重新渲染之前调用。它可以返回一个对象来更新 state,或者返回 null 来表示新的 props 不需要任何 state 的更新。

B. getSnapshotBeforeUpdate

如果是如下的写法:

// 在更新之前获取快照
getSnapshotBeforeUpdate(prevProps, prevState) {
   console.log('getSnapshotBeforeUpdate')
}

【React】深入理解React组件生命周期----图文详解(含代码)_第11张图片

当点击 点我+1的按钮时,会出现报错,原因是:一个snapshot value,或者null必须得返回。但是却返回一个undefined。说明,这个getSnapshotBeforeUpdate钩子函数必须要有返回值

正确的写法:

// 在组件更新之前获取快照 
// 快照可以是任何值,如字符串,数组,对象,函数
getSnapshotBeforeUpdate(prevProps, prevState) {
   console.log('getSnapshotBeforeUpdate')
   return 'atguigu'   //这个返回值将作为snapshotValue传给componentDidUpdate
}

在官方文档中规定:

  • getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(备份之前的数据)(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()
  • 此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。
  • 应返回 snapshot 的值(或 null)。

解释:

  1. snapshot value是组件在发生更新之前从DOM中捕获的一些信息,如表格高度,滚动条的位置等。

  2. getSnapshotBeforeUpdate生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()

    // 组件更新完毕的钩子
    componentDidUpdate(preProps, preState, snapshotValue) {
      console.log('Count---componentDidUpdate', preProps, preState, snapshotValue)
      //打印结果:{count:'199'} {count:0} atguigu
    }
    

C.getSnapshotBeforeUpdate使用场景

效果:

【React】深入理解React组件生命周期----图文详解(含代码)_第12张图片

需求:

  • 在一个固定高度(150px)的容器中,每隔1s出现一条新闻(30px),并显示在最上方,超出高度的新闻以滚动条的形式显示,类式朋友圈,微博动态。
  • 鼠标控制滚动条滚动到某一位置,界面不动,不会自动跳转到最顶部新出现的新闻上去。

代码实现:

css:


jsx:

<script type="text/babel">
    class NewsList extends React.Component {
      state = { newsArr: [] }

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

      getSnapshotBeforeUpdate(prevProps, prevState) {
        return this.refs.list.scrollHeight   //拿到更新之前的内容区的高度
      }

      componentDidUpdate(prevProps, prevState, height) {
        //新的新闻出现之后的高度减去之前的高度
        this.refs.list.scrollTop += this.refs.list.scrollHeight - height 
        // this.refs.list.scrollTop = this.refs.list.scrollTop + this.refs.list.scrollHeight - height
        // 6:  30 = 0 + 180 - 150
        // 7:  60 = 30+ 210 - 180
      }

      render() {
        return (
          <div>
            <div className="list" ref="list">
              { //map()实现遍历,map()方法定义在JavaScript的Array中,
                //它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
                this.state.newsArr.map((news, index) => {
                  return <div key={index} className="news">{news}</div>
                })
              }
              {/* 
                  
新闻7
新闻6
新闻5
新闻4
新闻3
新闻2
新闻1
*/
} </div> </div> ); } } ReactDOM.render(<NewsList />, document.getElementById('test')) </script>

scrollHeight:这个只读属性指的是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容

scrollTop:指的是一个元素的内容顶部到它的可视窗口的顶部的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0

解释代码:

  • 当新闻的条数总高度超出容器的高度时,会溢出,需要在容器的样式中加上:overflow: auto;(当内容溢出时,会提供一个滚动条)
  • 首先初始化一个空数组的状态,当组件一挂载,开启循环定时器(每隔1s显示一条新闻),获取原始的状态(看看原来有什么新闻),再模拟一条新闻,再更新状态里的newsArr这个数组,把新的新闻和原先的新闻拼接一起。
state = { newsArr: [] }

componentDidMount() {
   setInterval(() => {
   // 获取原状态
   const { newsArr } = this.state
   // 模拟一条新闻
   const news = '新闻' + (newsArr.length + 1)
   // 更新状态
   this.setState({ newsArr: [news, ...newsArr] })
   //news:新的新闻,...newsArr:原先的新闻
	}, 1000)
}
  • 状态维护好之后,要驱动页面显示,所以在render里实现动态地遍历,使用map()实现遍历,这样就能实现 每隔1s出现一条新闻,并显示在最上方, 超出高度的新闻以滚动条的形式显示 这一功能了。
{ //map()实现遍历,map()方法定义在JavaScript的Array中,
  //它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
this.state.newsArr.map((news, index) => {
    return <div key={index} className="news">{news}</div>
	})
}
  • 要实现 鼠标控制滚动条滚动到某一位置,界面不动,不会自动跳转到最顶部新出现的新闻上去 这一功能,必须用到getSnapshotBeforeUpdate这个钩子,在更新之前拿到内容区的高度 ,传给componentDidUpdate这个钩子作为第三个参数,最关键的一步,在componentDidUpdate这个钩子里实现上述这个功能:动态地调整这个this.refs.list.scrollTop往上窜地高度:拿到更新之后的内容区高度,减去更新之前的内容区高度,加上原先计算前的scrollTop 就能设置新加的元素往上窜地高度,从而实现上述功能。
componentDidUpdate(prevProps, prevState, height) {
  //新的新闻出现之后的高度减去之前的高度
  this.refs.list.scrollTop += this.refs.list.scrollHeight - height 
  // this.refs.list.scrollTop = this.refs.list.scrollTop + this.refs.list.scrollHeight - height
}

总结:

生命周期的三个阶段(新)如下:

  1. 初始化阶段:由ReactDOM.render()触发—初次渲染

    • constructor()
    • getDerivedStateFromProps()
    • render()
    • componentDidMount()
  2. 更新阶段:由组件内部this.setSate()或父组件重新render触发

    • getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()
  3. 卸载组件:由ReactDOM.unmountComponentAtNode()触发

    • componentWillUnmount()

6️⃣ 重要的三个钩子

  1. render:初始化渲染或更新渲染调用
  2. componentDidMount:开启监听, 发送ajax请求
  3. componentWillUnmount:做一些收尾工作, 如: 清理定时器

7️⃣ 即将废弃的钩子

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate

现在使用会出现警告,下一个大版本需要加上UNSAFE_前缀才能使用,以后可能会被彻底废弃,不建议使用。

你可能感兴趣的:(React,react.js,javascript,前端,前端框架)