上节课我们学习了如果不用柯里化的方法来处理我的回调,这节课我将按照学习路线往下继续介绍生命周期相关知识。
回顾
上节课的内容并没有什么重要的东西需要回顾,仅仅只需要记住一个原则,onXxxx
的回调必须是一个函数。
好了接下来我们开始介绍关于生命周期相关的内容。
效果需求
其实组件的生命周期是react
中一个最重要的一个概念。如果说你学习了react
但是你却不懂react
的生命周期,那几乎等于没学。
那么什么是生命周期?我们通过一个案例来引出生命周期这个概念。那么我们的案例需要实现什么效果呢?
- 页面上包含两个元素
- 一段文字
- 一个按钮
- 文字要渐渐从 1 ~ 0 修改透明度,当透明度为 0 时,直接修改为 1 然后再次渐渐向 0 修改
- 要求这个改变持续循环
- 透明度从 1 ~ 0 要求耗时
2s
- 点击按钮则清空整个页面显示的元素
页面实现
基础页面实现
现在我们知道需求了,那么我们一步一步来实现,先实现页面基本展示元素:
class TextComponent extends React.Component { render() { return ( <div> <h2>The test text</h2> <button>clean</button> </div> ); } } ReactDOM.render(<TextComponent />, document.getElementById("test"));
这样这样是不是就可以直接把最基本的页面展示渲染出来,我们来看一下页面效果:
那我们想一想,接下来我们该干什么?我们是不是还有两个功能要做?一个是字体的透明度循环改变,另一个是按钮的点击事件?那我们先做哪个后做哪个呢?咱也别上来就搞得那么复杂,咱们先来做简单的,这个点击事件是不是更简单一点?那么这个点击事件怎么实现?
卸载组件
首先第一步我们给<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
。那么这么写到底行不行呢?我们试试呗,看一下效果:
我们看一下,当我们点击了页面呗清空了,再看一下控制台:
控制台中也没有展示出有任何组件的存在。那么我们这个点击事件就算完成了。至此我们学会了如何去卸载组件。
修改文字透明度
当我们实现了点击事件的功能之后,是不是还剩下一个功能没有实现?这个透明度我们怎么改?
我来思考一个问题,页面上这个透明度在改变的时候页面有更新吗?当然是有更新的,那么页面的更新依赖与什么?是不是靠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 ?
那么好,我们来写一个循环定时器:
但是问题来了,为什么报红了啊?因为循环定时器是一个内置函数,而且我们在类的作用域空间里面能这么写吗?类里只能写构造器,自定义方法,访问器,和赋值语句。这个内置函数我们是不可以直接写在类作用域空间里面的。那么我们怎么办?那就换个地方呗,我们类里还有render
和clean
方法呢,这俩都是函数,函数体里面可以写的啊那么我们写在哪个函数体里面。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.1
再setState
更新进去,那么好,看我们代码中的写法,我用了-=
运算符,那么我是不是就不能像之前那样用const
来赋值了?而且我这个对象的key
和value
的变量名是一样的,这样是不是就可以使用对象的简写方式,就不用写成{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 ,那么就重置。好我们看一下效果。
这是为什么呢?我们不仅没能重置,而且opacity
还变成了负数。大家回想一下js
中0.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
方法中肯定是不合适的。那么我们放在那?
我们先来回顾一下类组件渲染流程:
React
解析组件标签React
发现这个组件是类组件React
是实例化这个类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
指向丢失的问题,不需要用赋值语句配合箭头函数。那么我们来看一下这次效果怎么样?
由于我这边没法录制动图,具体的效果大家自己来看一下吧。现在我们是不是已经完成了我们的需求?可能到这大家都会认为我们已经完成了,其实不是,我们点击一下clean
按钮看一下:
组件被卸载了,但是控制台给了错误警告。什么意思呢?说不能在一个被卸载的组件里执行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
,从而清空定时器。我们这次来看一下效果:
组件卸载了,而且也没有错误警告。其实我们还可以换一种方式。
我们说了componentDidMount
是在组件挂载完成的时候调,render
在组件挂载和state
更新时候调。那么真的就没有组件在接收到卸载指令之后,在卸载之前调的一个方法吗?其实是有的componentWillUnmount
,这个方法就是组件将要卸载的时候调用,那么我们把清除定时器的操作卸载这个方法中是不是很合适呢?那我们来试试
class TextComponent extends React.Component { state = { opacity: 1 }; componentDidMount() {...} render() {...} clean = () => {...}; componentWillUnmount(){clearInterval(this.timer);} }
我们直接在componentWillUnmount
中执行了clearInterval
方法,而且还是和之前一样通过this
来调用被我们挂在实例自身的timer
属性。那么我们来看一下效果:
也是一切正常。那么到了这一步我们才算是完成了我们的需求。
引出生命周期
我们之前这么多内容终于算是把我们的这个案例给完成了,那么这节课我们的目的不是来完成这个案例的。我们是要说生命周期的概念,什么是生命周期?
我们来回想一下,当我们要把一个组件渲染到页面上,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/