上节课我们学习了如果不用柯里化的方法来处理我的回调,这节课我将按照学习路线往下继续介绍生命周期相关知识。

回顾

上节课的内容并没有什么重要的东西需要回顾,仅仅只需要记住一个原则,onXxxx的回调必须是一个函数。

好了接下来我们开始介绍关于生命周期相关的内容。

效果需求

其实组件的生命周期是react中一个最重要的一个概念。如果说你学习了react但是你却不懂react的生命周期,那几乎等于没学。

那么什么是生命周期?我们通过一个案例来引出生命周期这个概念。那么我们的案例需要实现什么效果呢?

  1. 页面上包含两个元素
  2. 一段文字
  3. 一个按钮
  4. 文字要渐渐从 1 ~ 0 修改透明度,当透明度为 0 时,直接修改为 1 然后再次渐渐向 0 修改
  5. 要求这个改变持续循环
  6. 透明度从 1 ~ 0 要求耗时 2s
  7. 点击按钮则清空整个页面显示的元素

页面实现

基础页面实现

现在我们知道需求了,那么我们一步一步来实现,先实现页面基本展示元素:

class TextComponent extends React.Component {
  render() {
    return (
      <div>
        <h2>The test text</h2>
        <button>clean</button>
      </div>
    );
  }
}
ReactDOM.render(<TextComponent />, document.getElementById("test"));

这样这样是不是就可以直接把最基本的页面展示渲染出来,我们来看一下页面效果:

image-20211229132727597

那我们想一想,接下来我们该干什么?我们是不是还有两个功能要做?一个是字体的透明度循环改变,另一个是按钮的点击事件?那我们先做哪个后做哪个呢?咱也别上来就搞得那么复杂,咱们先来做简单的,这个点击事件是不是更简单一点?那么这个点击事件怎么实现?

卸载组件

首先第一步我们给<button>加上一个onClick={this.clean},接下来我们来定义clean方法,但是这个方法里面怎么写?我们截止到目前学得都是怎么在页面上显示这个组件,用ReactDOM.render,但是我现在想要清除掉这个组件怎么办?

其实我们通常都说把组件渲染到页面上,但是其实这个操作还有一个更官方的说法叫做挂载(mount)组件。那么有挂载就有卸载(unmount)。我们刚才所说的叫清除页面显示元素,本质上是不是就是要把这个组件给清除掉?这个清除说得官方一点其实就是卸载组件。当然这两个词大家也不用太纠结,就是我们本来就理解的意思,只不过换了个说法而已。

那么我们现在clean方法的作用是不是就是要卸载组件?那么怎么卸载?我也就不卖关子了,react也提供了现成的方法叫ReactDOM.unmountComponentAtNode,这个方法需要传入DOM节点,不然的话react不知道你到底要卸载哪个容器里的组件。我们渲染组件的时候是把组件渲染到了<div id="test">节点,那么自然我们也要把<div id="test">DOM节点传到unmountComponentAtNode方法中去。那么怎么传呢?和render一样啊,document.getElementById("test"),那么我们把代码改一下:

class TextComponent extends React.Component {
  render() {
    return (
      <div>
        <h2>The test text</h2>
        <button onClick={this.clean}>clean</button>
      </div>
    );
  }
  clean = () => { 
    ReactDOM.unmountComponentAtNode(document.getElementById("test"));
  };
}

可能有人要说了,我们不是要避免直接操作真实DOM吗?这里这个document这也太扎眼了。能不能不写document?我们没有办法这个容器节点本来就是一个真实DOM,要是想要获取到这个节点,我们肯定是要操作真实DOM。那么这么写到底行不行呢?我们试试呗,看一下效果:

image-20211229135720000

我们看一下,当我们点击了页面呗清空了,再看一下控制台:

image-20211229135827577

控制台中也没有展示出有任何组件的存在。那么我们这个点击事件就算完成了。至此我们学会了如何去卸载组件。

修改文字透明度

当我们实现了点击事件的功能之后,是不是还剩下一个功能没有实现?这个透明度我们怎么改?

我来思考一个问题,页面上这个透明度在改变的时候页面有更新吗?当然是有更新的,那么页面的更新依赖与什么?是不是靠state的更新来驱动页面的更新?那么好我们要用到state了,使用state的第一步要初始化state。我们这个页面上的文字的透明度是不是一直在变?而且初始透明度是 1 。那么我们初始化state就只需要一个属性opacity就可以了初始状态设为 1 。那么透明度持续在变,也就代表着,state一直在变,那么我们直接用state来驱动页面来改变文字的style就可以了啊,那么我们就要给<h2>加一个style属性:

class TextComponent extends React.Component {
  state = { opacity: 1 };
  render() {
    return (
      <div>
        <h2 style={{opacity: this.state.opacity}}>The test text</h2>
        <button onClick={this.clean}>clean</button>
      </div>
    );
  }
  clean = () => {...};
}

这样我们是不是就可以通过改变state来改变<h2>的透明度了?但是这个透明度是一直在变的,而且要在2s内实现从 1 ~ 0 的变动,那么我们是不是要开启一个循环定时器?每隔一段时间让透明度减少一点直到变为 0 ?

那么好,我们来写一个循环定时器:

image-20211229141622232

但是问题来了,为什么报红了啊?因为循环定时器是一个内置函数,而且我们在类的作用域空间里面能这么写吗?类里只能写构造器,自定义方法,访问器,和赋值语句。这个内置函数我们是不可以直接写在类作用域空间里面的。那么我们怎么办?那就换个地方呗,我们类里还有renderclean方法呢,这俩都是函数,函数体里面可以写的啊那么我们写在哪个函数体里面。clean方法是干什么的?是不是卸载组件的?而且在点击了clean按钮才会触发,那我在卸载了组件的时候开定时器有意义吗?所以我们肯定卸载render方法里面啊,那么好,我们来修改一下代码:

class TextComponent extends React.Component {
  state = { opacity: 1 };
  render() {
    setInterval(() => {
      let { opacity } = this.state;
      opacity -= 0.1
      this.setState({ opacity });
    }, 200);
    return ...
  }
  clean = () => {...};
}

大家看完上面的代码,在render方法中来开启一个循环定时器,这个函数接收一个函数和一个间隔时间。如果我们每次要减 0.1 的透明度是不是得每200ms减一次?所以我们的间隔时间就设置为 200,然后前面的函数是我们这个定时器要做的事情,那么我们在函数体中来获取到当前state,然后减去0.1setState更新进去,那么好,看我们代码中的写法,我用了-=运算符,那么我是不是就不能像之前那样用const来赋值了?而且我这个对象的keyvalue的变量名是一样的,这样是不是就可以使用对象的简写方式,就不用写成{opacity: opacity}了。

但是上面这一段还有个瑕疵。是不是减到 0 之后还会继续减?透明度哪有负数啊?所以我们是不是就应该做一个限制。而且我们的要求是一旦透明度到了 0 之后就要重置透明度,那么我们改一下代码:

class TextComponent extends React.Component {
  state = { opacity: 1 };
  render() {
    setInterval(() => {
      let { opacity } = this.state;
      opacity = opacity = 0 === opacity ? 1 : opacity - 0.1;
      this.setState({ opacity });
    }, 200);
    return ...
  }
  clean = () => {...};
}

我们用三元运算符来做一个判断,如果当前state中的opacity为 0 ,那么就重置。好我们看一下效果。

image-20211229145340859

这是为什么呢?我们不仅没能重置,而且opacity还变成了负数。大家回想一下js0.1 + 0.2等于0.3吗?是不是有遇到了那个坑死无数人的那个面试提了?精度丢失问题。那我们怎么处理?来看一下:

class TextComponent extends React.Component {
  state = { opacity: 1 };
  render() {
    setInterval(() => {
      let { opacity } = this.state;
      opacity = opacity = 0 >= opacity ? 1 : opacity - 0.1;
      this.setState({ opacity });
    }, 200);
    return ...
  }
  clean = () => {...};
}

我们是不是要这个文字完全透明才会重置透明度?透明度小于 0 的时候是不是也是完全透明?那么我们判断当前透明度是不是小于等于 0 不就万事大吉了嘛。但是呢,大家谨慎一点,我先说一下这个效果就不截图展示了:打开这个页面的时候,会发现页面上<h2>透明度变化的频率会越来越快,根本不像我们想象中那样每2s循环一次。大家一定要谨慎测试这个页面,因为这个页面一旦开启,你电脑CPU的温度就会直线飙升。

但是为什么会这样?我们这个定时器开得很合理啊。在render方法中,开了定时器,渲染页面的时候,react实例化了组件,并通过组件实例对象来调用render方法,然后开启了定时器,很合理的啊。因为我们在这里导致了一个无限递归。什么意思?

我问大家一下,render方法在什么时候被调用?是不是组件挂载到页面的时候调用一次,然后state每更新一次就调用一次?那么好,render方法中开启了定时器,每200ms更新一次state,是不是就代表每200ms调用一次render方法?也就意味着每200ms开启一个新的定时器,而而旧的定时器并没有被释放。所以说我定时器放在render方法中肯定是不合适的。那么我们放在那?

我们先来回顾一下类组件渲染流程:

  1. React解析组件标签
  2. React发现这个组件是类组件
  3. React是实例化这个类
  4. React通过组件实例对象调用render

好,到这就可以了。其实react在挂载组件的时候并不是只调用了render方法,在组件完成挂载之后,react还自动通过组件实例对象帮我们调用了一个方法叫componentDidMount,这个方法从一个组件挂载到组件卸载之间这一个周期中就只调用一次。什么时候调用?就在组件完成挂载之后马上就通过组件的实例对象来调用。

那么好,回头再聊聊我们为什么说定时器不能放在render方法里?因为render会被调用1 + N次,会开启无数个新的定时器。而我们的componentDidMount方法被调几次?是不是只有一次?而且还是在组件完成挂载之后马上就调。关键我们刚好还要求组件完成挂载之后马上开启定时器。那么我们是不是可以吧这个定时器卸载componentDidMount方法里?那么我们来看代码

class TextComponent extends React.Component {
  state = { opacity: 1 };
  componentDidMount() {
    setInterval(() => {
      let { opacity } = this.state;
      opacity = 0 >= opacity ? 1 : opacity - 0.1;
      this.setState({ opacity });
    }, 200);
  }
  render() {return ..}
  clean = () => {...};
}

可能有人要问了,为什么这里componentDidMount不写赋值语句配合箭头函数了呢?我们来想一想,之前为什么要用赋值语句和箭头函数啊?这个写法是不是因为我们自定义的方法要作为事件回调来用,作为事件回调的话,调用我们自定义方法的方式就不是通过类的实例对象调用的了,所以说要使用赋值语句配合箭头函数来解决this指向丢失的问题。但是这个componentDidMount是怎么调用的?在组件完成挂载之后,react通过组件的实例对象来调用的,而且只会调用一次。所以说这里不会出现this指向丢失的问题,不需要用赋值语句配合箭头函数。那么我们来看一下这次效果怎么样?

image-20211229160300750

由于我这边没法录制动图,具体的效果大家自己来看一下吧。现在我们是不是已经完成了我们的需求?可能到这大家都会认为我们已经完成了,其实不是,我们点击一下clean按钮看一下:

image-20211229160811329

组件被卸载了,但是控制台给了错误警告。什么意思呢?说不能在一个被卸载的组件里执行state的更新。我们也很容易理解,这个组件都没了,那还更新什么state啊?那么怎么处理呢?

我们来想一想导致这个错误的原因是什么?是不是我们定时器开了就没有关闭过?我们在组件挂载的时候开了一个定时器,没错,是开了,但是我们却从来都没有关闭过,当我们点了clean按钮之后,组件卸载了,但是定时器依然没有关闭,还在尝试去更新state这个时候那还有state供定时器更新啊?那么最终的结果就是提示错误。

所以说我们是不是应该清空定时器?那么什么时候清空?卸载组件之后清空吗?大家有没有觉得在卸载组件之后和清空定时器这一步操作之间有可能还会执行一次对state的更新?所以说我们是不是应该在组件卸载之前清空?那么好,我们怎么知道我要卸载这个组件了?是不是有<clean>onClick在监听着?那么我们在<clean>onClick回调中先清空定时器然后再卸载组件,那不是刚好满足我们的需求嘛,那么好,我们来看代码怎么写:

class TextComponent extends React.Component {
  state = { opacity: 1 };
  componentDidMount() {
    this.timer = setInterval(...);
  }
  render() {return ...}
  clean = () => {
    clearInterval(this.timer);
    ReactDOM.unmountComponentAtNode(document.getElementById("test"));
  };
}

我们都知道清空定时器有个内置函数叫clearInterval,但是我们是不是要把那个定时器传给这个函数。但是我们怎么拿到这个定时器?仿佛没有什么好办法,我们在clean方法的作用域内是不是只能拿到类实例对象自身的属性以及clean方法作用域内定义的变量,我是不是没法跨作用域去拿到componentDidMount方法中的定时器?那么好,在这两个方法的作用域中有一个共同的东西,就是this,有人说,你怎么知道这两个方法中的this是一样的呢?我来问你,clean方法中的this指向是谁?赋值语句配合箭头函数来解决this指向丢失问题,是不是最终结果使得clean方法中的this是类组件的实例对象?而componentDidMount方法是通过类组件的实例对象调的,所以componentDidMount方法中的this是不是也是类组件的实例对象?,那么我们是不是可以直接在componentDidMount方法中把定时器赋给类组件实例对象自身的一个timer属性?这样的话我就可以在clean方法中通过this来获取到timer并且传入到clearInterval,从而清空定时器。我们这次来看一下效果:

image-20211229163853766

组件卸载了,而且也没有错误警告。其实我们还可以换一种方式。

我们说了componentDidMount是在组件挂载完成的时候调,render在组件挂载和state更新时候调。那么真的就没有组件在接收到卸载指令之后,在卸载之前调的一个方法吗?其实是有的componentWillUnmount,这个方法就是组件将要卸载的时候调用,那么我们把清除定时器的操作卸载这个方法中是不是很合适呢?那我们来试试

class TextComponent extends React.Component {
  state = { opacity: 1 };
  componentDidMount() {...}
  render() {...}
  clean = () => {...};
  componentWillUnmount(){clearInterval(this.timer);}
}

我们直接在componentWillUnmount中执行了clearInterval方法,而且还是和之前一样通过this来调用被我们挂在实例自身的timer属性。那么我们来看一下效果:

image-20211229164822339

也是一切正常。那么到了这一步我们才算是完成了我们的需求。

引出生命周期

我们之前这么多内容终于算是把我们的这个案例给完成了,那么这节课我们的目的不是来完成这个案例的。我们是要说生命周期的概念,什么是生命周期?

我们来回想一下,当我们要把一个组件渲染到页面上,react是不是好像一直在一个合适的时间点来做了一些合适的事儿?当我们挂载组件时react是不是得调用render方法?只要挂载完成了,react又会自动去调用componentDidMount方法,当这儿个组件即将被卸载的时候react又去主动调用了componentWillUnmount方法。

这样一整套流程下来有没有觉得这就仿佛是一个人的一生?来到这个世界上,第一声啼哭,接收世界上的外来教育,留下在这个世界上的最后一句话,然后悄然离世。

而组件的生命周期就如同人的一生,在关键的状态来调用特定的函数,在这些函数中来做特定的事。比如我们的案例一旦挂载完成就开启定时器,即将卸载的时候就清空定时器。这就是一个完整的生命周期。

但是生命周期还有一些特殊叫法

  • 生命周期函数
  • 生命周期回调函数
  • 生命周期钩子函数
  • 生命周期钩子

这些都是同一个意思。

总结

  • 组件渲染就是组件挂载,清空组件就是组件卸载
  • 组件在挂载完成之后会自动调用componentDidMount方法
  • 组件在卸载之前会自动调用componentWillUnmount

以上那个便是我们这节课的内容。

Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.

Link of this article:https://work.lynchow.com/article/lifecycle_start/