React小书学习笔记 —— 挂载阶段的组件生命周期(二)

前言

学习前端一小步,迈向成功一大步!本专栏主要记录学习前端React框架的一些个人心得,分享一些实战教学,如有不足,欢迎交流讨论。React框架的入门教学强推胡子大哈React小书,简单易懂还有代码实战。还等什么?让我们开始本篇的前端学习之旅,欢迎各位入坑前端!

挂载阶段的组件生命周期(二)

参考教程:React小书–第18节(挂载阶段的组件生命周期(二))
教程作者:胡子大哈
参考链接:React小书
本文搭配原文教程食用,风味更佳~!


经过上一节的讲述,你大概已经知道挂载阶段组件各生命周期有哪些,包括其排布的方式,调用的顺序等等,这节我们将接下来将讨论对于一个组件来说,其挂载阶段生命周期的几个方法(constructor ; componentWillMount; componentDidMount; componentWillUnmount)在一个组件的出生到死亡的过程里面起了什么样的作用。

constructor()

一般来说, constructor 里主要做一些关于组件自身状态的初始化工作。即所有组件的 state 的初始化工作都是放在 constructor 里面的,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class xxx extends Component {
constructor() {
super()
this.state = {
xx:yy,
xx:yy,
...
}
}

...

render() {
...
}
}

componentWillMount()

小书中提到,componentWillMount 生命周期主要用于一些组件启动的动作,比如Ajax 数据的拉取操作、一些定时器的启动等,此外,书中还举了一个Ajax的例子:

1
2
3
4
5
6
7
...
componentWillMount () {
ajax.get('http://json-api.com/user', (userData) => {
this.setState({ userData })
})
}
...

但是!问题来了: 上一节我们不是说componentWillMount是在render前执行,无法触发setState的二次渲染吗?那这例子又是什么例子呢?对此,这里引出另一篇参考文献(React componentwillmount和componentdidmount请求数据)来深入了解一下。
首先要明确一点,在componentWillMount中执行this.setState是不会触发二次渲染的。 它也只会在挂载过程中被调用一次,它的作用和constructor没有太大差异。
在componentWillMount中请求后台数据是无法保证更早得到数据的,因为componentWillMout是在render函数执行前执行的,虽然请求是在第一次render之前发送的,但是返回并不能保证在render之前完成(即按照js异步的特点,在请求数据时,render已经开始异步工作了),render不会等你慢慢请求.所以在渲染的时候没有办法等到数据到来再去setState触发二次渲染。
那为什么小书中说可以用 componentWillMount 进行 Ajax 数据拉取的操作呢?这是因为在服务端渲染的场景中componentDidMount是不会被执行的,因此可以在componnetWillMount中发送AJAX请求,注意这里是发送请求,而非发送并接收到请求,举个例子:你尝试过自己给自己送信吗?如果你要自己给自己送信会怎么办?方法很简单,你写一封信从浙江寄到武汉,寄完马上乘动车从浙江出发,到达武汉后等着这封信送到你手中就可以了,当然这封信可能会比你早到,那么你去邮局取件完事。componentWillMount 和 render 的关系也是如此,componentWillMount 先发送请求,然后程序就开始执行 render,至于请求怎么处理的和 render 执行不存在冲突,render 完成后等待请求结果或者获取结果即可。
顺便说一句在es6中,使用extend component的方式里的constructor函数和componentWillMount是通用的作用,所以你在构造函数里初始化了组件的状态就不必在WillMount做重复的事情了.React中不推荐在componentWillMount中发送异步请求。

以下是对该片引文的重点提取。

  1. componentWillMount 是一个同步操作,即你只有进行了 componentWillMount 才能进行后续的 render 等一系列操作。
  2. componentWillMount 在 render 前执行,所以 componentWillMount 中执行 this.setState 无法触发二次渲染(此时组件自身都没被渲染)。
  3. componentWillMount 可以执行一些不依赖于组件渲染的操作,比如定时器启动,请求发送等。

引文还将 componentWillMount 和 componentDidMount 做了对比,相比于 componentWillMount ,componentDidMount 这个生命周期函数在是在render之后调用一次,component已经初始化完成了。在生产时,componentDidMount生命周期函数是最好的时间去请求数据,其中最重要原因:使用componentDidMount第一个好处就是这个一定是在组件初始化完成之后,再会请求数据,因此不会报什么警告或者错误,我们正常请教数据完成之后一般都会setState。

componentWillUnmout()

componentWillUnmount 生命周期主要在组件销毁前(注意是销毁前,可以看前一篇,是先执行 componentWillUnmout,后在页面删除组件),执行一些清场工作,比如清楚定时器等。在 React 中,组件生命周期是 js 自己控制的,父组件不要子组件了,那么子组件就可以卸载了,就这么简单。React 管理 DOM 的过程,可以理解为每个组件 render() 返回的虚拟 DOM 会被 React 整理到一个树里面,按照它们之间相互依赖的关系,把相应的组件 mount 起来。然后可能父组件状态变化之后, render() 不返回某个子组件了,那么这个子组件就会被 React unmount 掉。

动态时钟实战

下面记录了不堪入目的代码实战过程:

step1:创建React

1
2
npx create-react-app clock
cd clock

step2:定义时钟组件

创建 Clock.js (注意大写,语法规范:导出类名的文件开头首字母大写)
编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Clock.js代码:
import React,{Component} from 'react';

class Clock extends Component {
constructor() {
super()
this.state = {
date: new Date()
}
}

render() {
<div>
<h1>
<p>现在时间是</p>
{this.state.date.toLocaleTimeString()}
</h1>
</div>
}
}

export default Clock;
--------------------------------------------------------------
index.js代码:
import reportWebVitals from './reportWebVitals';

import React from 'react';
import ReactDOM from 'react-dom';
import Clock from './Clock';

ReactDOM.render(
<React.StrictMode>
<Clock />
</React.StrictMode>,
document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

稍作解释:
Clock.js 中首先要引入import React,{Component} from 'react';(基本每个React组件都要引用),创建组件,在组件构造函数生命周期中初始化 state 状态(初始化一个对象),new Date()定义了一个 Date 对象,在 render 中调用 Date 对象的.toLocaleTimeString()方法,根据本地时间格式,把 Date 对象的时间部分转换为字符串。可结合“Date 对象”一起学习。最后 export default xxx导出组件类
index.js 主要包含了 ReactDOM.render() 方法,需要 import React from 'react';import ReactDOM from 'react-dom';import Clock from './Clock';,然后导入组件进行渲染。

ok! npm start运行~

what!!!出错了,好像render()没有return,大意了。加上return后再运行(注意return后加 () )。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Clock.js代码:
import React,{Component} from 'react';

class Clock extends Component {
constructor() {
super()
this.state = {
date: new Date()
}
}

render() {
return (
<div>
<h1>
<p>现在时间是</p>
{this.state.date.toLocaleTimeString()}
</h1>
</div>
)
}
}

export default Clock;

step3:实现时间动态:

完美!但是现在时间是静态的,我们需要加上一个定时器,放在 componentWillMount() 生命周期中,每隔多少秒就重新更新一次state状态(实际操作过程中,发现vscode竟然没有自动提示componentWillMount(),果然是被淘汰了呀,不过幸好还能用)。

上述意思大致是:componentWillMount 被重命名为了 UNSAFE_componentWillMount,React 建议少用 componentWillMount,因为初始化状态可以在 constructor 中进行,其余的可以在 componentDidMount 中进行,所以没 componentWillMount 啥事了。

扯远了,继续撸代码,上步我们加了一个 componentWillMount,里面装了个定时器setInterval(),每个1000ms重新更新一下状态并渲染(this.setState),这里由于每秒都会渲染一次,所以不用担心componentWillMount 无法二次渲染的问题。

1
2
3
4
5
6
7
...
componentWillMount() {
this.timer = setInterval(() => {
this.setState({date: new Date()})
},1000)
}
...

step4:时间隐藏与显示

至此我们实现了时钟的动态展示,下一步我们实现时间的隐藏:
在 index.js 中新建一个 Index 类,用来包裹 Clock 组件,并设置一些隐藏的功能。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
index.js:
import reportWebVitals from './reportWebVitals';

import React,{Component} from 'react'; //加上了{Component}
import ReactDOM from 'react-dom';
import Clock from './Clock';

class Index extends Component {
constructor() {
super()
this.state = {
isShowClock: true
}
}

handleShowOrHide() {
this.setState({
isShowClock: !this.state.isShowClock
})
}

render() {
return (
<div>
{this.state.isShowClock?<Clock />:null}
<button onClick={this.handleShowOrHide.bind(this)}>显示/隐藏</button>
</div>
)
}
}

ReactDOM.render(
<React.StrictMode>
<Index />
</React.StrictMode>,
document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

需要注意的几点:

  1. this.setState()是一个方法,传入一个state对象,即用新的state更新上一时间的state状态(注意是更新而非替换,若新的状态不在原来的状态内,则是添加操作)。
  2. 组件中调用状态值都要用 this.state.属性名 的形式。
  3. 关注<div>{...}</div>的形式,这是典型的JSX书写,即HTML中嵌入JS代码。任何函数及表达式在JSX中都要写入 {} 内,例如onClick={...函数...}
  4. 注意 .bind(this),组件内部函数没有绑定 this 对象,所以需要人为绑定。箭头函数例外,箭头函数会自动指向 this。

更新完之后,我们运行代码:

step5:添加componentWillUnmout,清除定时器

隐藏和显示的功能我们已经实现了,但是F12调试发现居然报错了!

报错原因大致是:没有 unmount 的组件,容易造成内存泄露。结合小书学习我们可知,定时器在每次组件移除时并没有被清除,下一次组件生成时又创建了一个新的定时器,时钟隐藏的时候,定时器的回调函数还在不停地尝试 setState,由于 setState 只能在已经挂载或者正在挂载的组件上调用,所以 React.js 开始疯狂报错。这时候componentWillUnmount 就可以派上用场了,它的作用就是在组件销毁的时候,做这种清场的工作。例如清除该组件的定时器和其他的数据清理工作。我们给 Clock 添加 componentWillUnmount,在组件销毁的时候清除该组件的定时器:

1
2
3
4
5
6
Clock.js:
...
componentWillUnmount() {
clearInterval(this.timer)
}
...

注意:这行代码是写在 Clock.js 下的。
至此,我们完成了实战,时间能动态走动,且能隐藏或显示,并且不会报错。

总结

我们一般会把组件的 state 的初始化工作放在 constructor 里面去做;
在 componentWillMount 进行组件的启动工作,例如 Ajax 数据拉取、定时器的启动;
组件从页面上销毁的时候,有时候需要一些数据的清理,例如定时器的清理,就会放在 componentWillUnmount 里面去做。

没有提到的 componentDidMount 将在后续讲解。一般来说,有些组件的启动工作是依赖 DOM 的,例如动画的启动,而 componentWillMount 的时候组件还没挂载完成,所以没法进行这些启动工作,这时候就可以把这些操作放在 componentDidMount 当中。