前言
react
的hooks
是react 16.8
引入的一个新特性, 旨在让开发者可以在不使用class
的情况下使用state
和其他的react
特性, 例如生命周期
今天想和大家聊一聊useEffect
的具体使用方法, 一些需要预先了解的内容可以移步react hooks之useState查看, 这里就不再赘述了
概览
useEffect
是一个结合了componentDidMount
, componentDidUpdate
和componentWillUnmount
这3个class
组件生命周期的方法, 它给我们提供了在不写class
时使用上述1个或几个生命周期的能力, 同样的事, 使用uesEffect
只需更少的代码即可完成
语法
在正文开始之前, 我们依旧先来看看它的语法:
useEffect(
() => {
},
[]
);
它接收2个参数:
- (必填)第一个参数是个函数, 每次
useEffect
执行的时候就会执行这个函数 - (可选)第二个参数是依赖, 或者说是依赖数组,
uesEffect
会根据这个数组的内容的不同而在不同时机去执行第一个参数传入的函数
同时需要注意的是:
- 如果不传第二个参数, 那么组件每次渲染, 必填参数(函数)都会执行, 我们通常是需要让必填参数(函数)有条件的执行, 因此第二个参数一般都是需要传递的
- 无论我们是否传递第二个参数, 传的什么, 必填参数(函数)都至少会执行一次, 而且都是在页面渲染完毕之后执行, 也就是说初始化的时候必填参数(函数)就会执行, 至于接下来是否再次执行, 什么时候执行就取决于第二个参数传递的是什么了, 具体情况将在后面详述
接下来让我们动手写一写, 来感受一下这几个生命周期的不同写法所带来的差异
componentDidMount
相信诸位reacter
对这个生命周期一定不陌生, 我们会在这个生命周期中对服务端发起请求, 去获取数据, 或者访问DOM
节点
class
在class
中, 我们会这样写:
import React, { PureComponent } from 'react';
class BruceLee extends PureComponent {
state = {
name: '李小龙',
career: '武术家, 演员',
birthday: ''
}
componentDidMount() {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
}
render() {
const { name, career, birthday } = this.state;
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
<button
onClick={
() => {
this.setState({
birthday: '1940年11月27日'
});
}
}
>设置生日</button>
</div>
);
}
}
export default BruceLee;
不使用class
接下来我们着重看看不写class
, 也就是在函数式组件
中使用useEffect
的时候如何表示componentDidMount
, 先说结论, 这样方便查看:
当useEffect的第二个参数传[]的时候表示我们将其用作componentDidMount
具体代码如下:
import React, { useState, useEffect } from 'react';
const BruceLeeHook = () => {
const [ name, setName ] = useState('李小龙');
const [ career, setCareer ] = useState('武术家, 演员');
const [ birthday, setBirthday ] = useState('');
useEffect(
() => {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
},
[]
);
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
<button
onClick={
() => {
setBirthday('1940年11月27日');
}
}
>设置生日</button>
</div>
);
};
export default BruceLeeHook;
componentDidUpdate
这个生命周期相信大家也不会陌生, 它会在页面重新渲染之后执行
class
import React, { PureComponent } from 'react';
class BruceLee extends PureComponent {
state = {
name: '李小龙',
career: '武术家, 演员',
birthday: ''
}
componentDidMount() {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
}
componentDidUpdate() {
console.log('componentDidUpdate');
console.log(this.state.birthday);
}
render() {
const { name, career, birthday } = this.state;
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
<button
onClick={
() => {
this.setState({
birthday: '1940年11月27日'
});
}
}
>设置生日</button>
</div>
);
}
}
export default BruceLee;
点击按钮修改state
中birthday
的值, 页面重新渲染, render
方法执行, 然后进入到了componentDidUpdate
中
不使用class
接着还是着重来看看它在函数式组件
中的写法:
import React, { useState, useEffect } from 'react';
const BruceLeeHook = () => {
const [ name, setName ] = useState('李小龙');
const [ career, setCareer ] = useState('武术家, 演员');
const [ birthday, setBirthday ] = useState('');
useEffect(
() => {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
},
[]
);
useEffect(
() => {
console.log('componentDidUpdate');
console.log(birthday);
},
[ birthday ]
);
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
<button
onClick={
() => {
setBirthday('1940年11月27日');
}
}
>设置生日</button>
</div>
);
};
export default BruceLeeHook;
就如一开始提到的, 初始化的时候useEffect
的第一个参数(函数)就会执行一次, 我们可以看到一开始就会执行:
() => {
console.log('componentDidUpdate');
console.log(birthday);
}
这部分代码, 然后当我们修改生日的时候会再次执行, 同时这里需要留意, 第二个参数是个数组, 里面可以传多个值, 比如我这边再加一个state
:
const [ wisdom, setWisdom ] = useState('');
同时在render
方法中再额外修改一个state
:
<button
onClick={
() => {
setCareer('以演员为职业的武术家');
}
}
>设置职业</button>
然后useEffect
的代码修改如下:
useEffect(
() => {
console.log('componentDidUpdate');
console.log(birthday);
console.log(wisdom);
},
[ birthday, wisdom ]
);
那么最终代码如下:
import React, { useState, useEffect } from 'react';
const BruceLeeHook = () => {
const [ name, setName ] = useState('李小龙');
const [ career, setCareer ] = useState('武术家, 演员');
const [ birthday, setBirthday ] = useState('');
const [ wisdom, setWisdom ] = useState('');
useEffect(
() => {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
},
[]
);
useEffect(
() => {
console.log('componentDidUpdate');
console.log(birthday);
console.log(wisdom);
},
[ birthday, wisdom ]
);
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
{
wisdom &&
(
<div>{`我的名言是: ${wisdom}`}</div>
)
}
<button
onClick={
() => {
setBirthday('1940年11月27日');
}
}
>设置生日</button>
<button
onClick={
() => {
setWisdom('be water my friend');
}
}
>设置名言</button>
<button
onClick={
() => {
setCareer('以演员为职业的武术家');
}
}
>设置职业</button>
</div>
);
};
export default BruceLeeHook;
此时我们点击按钮会发现: 依赖中的任意一个值更新了, 函数就会执行, 但依赖之外的值改变则不会触发函数的执行
接下来, 我们着重看看这部分代码:
const BruceLeeHook = () => {
const [ birthday, setBirthday ] = useState('');
const [ wisdom, setWisdom ] = useState('');
useEffect(
() => {
console.log('componentDidUpdate');
console.log(birthday);
console.log(wisdom);
},
[ birthday, wisdom ]
);
//...
};
函数式组件
的函数体部分中useEffect
的函数里使用了birthday
和wisdom
, 这两个变量在BruceLeeHook
函数体顶部被定义, 不仅如此, 它们还被传递到了useEffect
的第二个参数中去, 这是因为我们要在birthday
或wisdom
改变的时候触发useEffect
的第一个参数执行, 但如果我们这么写呢:
const BruceLeeHook = () => {
const [ birthday, setBirthday ] = useState('');
const [ wisdom, setWisdom ] = useState('');
const [ career, setCareer ] = useState('武术家, 演员');
useEffect(
() => {
console.log('componentDidUpdate');
console.log(birthday);
console.log(wisdom);
console.log(career);
},
[ birthday, wisdom ]
);
//...
};
我们使用了一个在依赖数组
中不存在的变量career
, 此时我们点击按钮修改职业, 函数不会执行, 但可以看到界面上的职业确实被修改了, 再修改birthday
和wisdom
中的任意一个值, 此时函数执行了, 同时新的职业被打印出来了, 但我们平时在将useEffect
用作componentDidUpdate
的时候不这么写, 而是这么写:
//...
useEffect(
() => {
console.log('componentDidUpdate');
console.log(birthday);
console.log(wisdom);
console.log(career);
},
[ birthday, wisdom, career ]
);
//...
所有在函数中需要使用的值都需要写到依赖数组
中去, 这是官方文档useEffect的一个推荐写法:
If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous renders. Learn more about how to deal with functions and what to do when the array values change too often.
如果你采用某个值改变再触发函数的写法, 请确保依赖数组包含组件作用域的所有值(例如props和state), 这些值会随着时间的推移而改变, 并且被useEffect所使用, 否则, 你的代码将引用以前渲染中的陈旧值, 想要了解更多关于如何正确使用useEffect的函数和如何处理依赖数组值频繁变更问题, 可点击这两个链接查看: 在依赖数组中省略useEffect函数中所使用的值是否安全, 如何处理依赖频繁变更的问题
componentWillUnMount
这个生命周期一般用来清理事件绑定或者clearTimeout
和clearInterval
class
index.js
:
import React, { PureComponent } from 'react';
import BruceLee from './BruceLee';
class Index extends PureComponent {
state = {
switchComp: false
}
render() {
const { switchComp } = this.state;
return (
<div>
{
!switchComp
? <BruceLee />
: <div>abc</div>
}
<button
onClick={
() => {
this.setState({
switchComp: !switchComp
});
}
}
>切换组件</button>
</div>
);
}
}
export default Index;
BruceLee.js
:
import React, { PureComponent } from 'react';
class BruceLee extends PureComponent {
state = {
name: '李小龙',
career: '武术家, 演员',
birthday: ''
}
componentWillUnmount() {
console.log('componentWillUnmount');
}
render() {
const { name, career, birthday } = this.state;
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
<button
onClick={
() => {
this.setState({
birthday: '1940年11月27日'
});
}
}
>设置生日</button>
</div>
);
}
}
export default BruceLee;
切换组件, 导致BruceLee
卸载的时候会触发componentWillUnmount
生命周期
不使用class
依赖数组传空数组
接下来还是着重来看看使用useEffect
的写法, index.js
中的代码保持不变, 修改BruceLee.js
:
import React, { useState, useEffect } from 'react';
const BruceLeeHook = () => {
const [ name, setName ] = useState('李小龙');
const [ career, setCareer ] = useState('武术家, 演员');
const [ birthday, setBirthday ] = useState('');
useEffect(
() => {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
return () => {
console.log('componentWillUnmount');
};
},
[]
);
useEffect(
() => {
console.log('componentDidUpdate');
console.log(birthday);
},
[ birthday ]
);
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
<button
onClick={
() => {
setBirthday('1940年11月27日');
}
}
>设置生日</button>
</div>
);
};
export default BruceLeeHook;
当点击切换组件的时候, 组件BruceLee
卸载的时候componentWillUnmount
字符串会被打印, 但这个写法相对独特, 接下来我们着重来看一下:
useEffect(
() => {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
return () => {
console.log('componentWillUnmount');
};
},
[]
);
我们可以看到, 在useEffect
的第一个参数中return
了一个函数, 这个函数就是我们的componentWillUnmount
生命周期
同时这里的依赖数组
为[]
, 那么就意味着这个useEffect
将在组件上树之后执行, 后续就不会再执行, 此时试想一下: 如果我们不传空数组呢?
依赖数组为非空数组
index.js
依旧不变, 修改BruceLee.js
:
import React, { useState, useEffect } from 'react';
const BruceLeeHook = () => {
const [ name, setName ] = useState('李小龙');
const [ career, setCareer ] = useState('武术家, 演员');
const [ birthday, setBirthday ] = useState('');
const [ wisdom, setWisdom ] = useState('');
useEffect(
() => {
console.log('componentDidMount');
const nameNode = document.getElementById('name');
console.log(nameNode);
},
[]
);
useEffect(
() => {
console.log('componentDidUpdate');
return () => {
console.log('componentWillUnMount');
};
},
[ career, birthday, wisdom ]
);
return (
<div>
<div id="name">{`我叫${name}`}</div>
<div>{`我是一名${career}`}</div>
{
birthday &&
(
<div>{`我的生日是: ${birthday}`}</div>
)
}
{
wisdom &&
(
<div>{`我的名言是: ${wisdom}`}</div>
)
}
<button
onClick={
() => {
setBirthday('1940年11月27日');
}
}
>设置生日</button>
<button
onClick={
() => {
setWisdom('be water my friend');
}
}
>设置名言</button>
<button
onClick={
() => {
setCareer('以演员为职业的武术家');
}
}
>设置职业</button>
</div>
);
};
export default BruceLeeHook;
关键代码如下:
useEffect(
() => {
console.log('componentDidUpdate');
return () => {
console.log('componentWillUnMount');
};
},
[ career, birthday, wisdom ]
);
根据上面的知识我们知道, 这个函数将在[ career, birthday, wisdom ]
三个值中任意一个改变的时候执行, 然后我们修改这3个值, 每次修改, 上面的关键代码都会执行, 但此时有个现象, 就是都会先打印componentWillUnMount
然后再打印componentDidUpdate
, 这是为什么呢?
查看文档Cleaning up an effect之后我们发现, 这是因为:
previous effect is cleaned up before executing the next effect
上一个effect将会在下一个effect执行之前被清理
也就是说:
- 我们第一次, 初始化的时候
console.log('componentDidUpdate');
执行了 - 而接下来更新
依赖数组
的值, 在第二次执行console.log('componentDidUpdate');
之前, 第一次return
的函数会先执行, 也就是会执行console.log('componentWillUnMount');
- 然后才会执行第二次的
console.log('componentDidUpdate');
, 以此类推
就像官方文档里提到的clean up
, 这个return
的方法将在下一次effect
执行之前先被执行, 这在实际的编码过程中会很有用, 写点伪代码, 比如:
let pivot = 0;
const BruceLeeHook = () => {
const handlePivote = () => {
//因为某些原因修改了pivot的值
pivot = 1;
};
useEffect(
() => {
if(pivot === 0) {
//做一些什么
}
return () => {
pivot = 0;
};
},
[]
);
};
export default BruceLeeHook;
大意就是:
- 一开始我们使用某个值做一些事, 期间修改了这个值, 那么条件不符, 就不再做这件事
- 而当组件再次上树的时候又要使用初始值再来做一开始做的事(比如
单页面应用
路由的切换) - 但此时这个值已经在第
1
步被修改过了, 那么此时就需要在componentWillUnMount
这个生命周期中做一个重置或者说清理的工作, 使之被重置成初始值 - 这样组件再次上树之后才能符合预期地走第
1
步的逻辑, 如此往复
好的, 那么这就是这篇文章的全部内容了, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需
参考文献: