全面理解useEffect——useEffect用法解析与使用技巧

全面理解useEffect

useEffect可以说是react的hook中最强大的一个,他能够以一种极为优雅的方式模拟我们的生命周期。但是优雅的代价就是这个hook极其的晦涩,不容易被理解与掌握。下面我们就来全方位的探讨一下,useEffect到底怎么用

useEffect的结构

在上一篇关于react的文章我提到过,函数式组件每次reRender都会重新执行一遍,如果我们需要生命周期的效果,那我们就需要useEffect函数。我们先来看一下useEffect的结构:

1
useEffect(setup, dependencies?)
  • 参数

    • setup:一个实现了你需要的副作用逻辑的函数,这个函数将会在组件首次被绑定以及每次重新渲染导致dependencies更新时调用。同时他还可以return一个清除函数,这个函数将会在函数卸载以及每次重新渲染导致dependencies更新之前调用

    • dependencies:在setup中引用的所有reactive values(state、参数以及在组件内部定义的变量和函数)或更新凭据组成的数组。这个数组必须是有限的并且偏平的,每次更新react将使用Object.is()比较每个依赖项是否与之前相同,如果不同则更新。

  • return

    • undefinded (无返回值)

中文博客中经常会漏掉setup函数的返回值,但是它的返回值是这个函数的半壁江山,我们决不能将其忽略不提,如果忽略掉的话我们就有一半的效果无法实现了。

❗:useEffect也是一个hook,请在组件或者自定义hook的最上层使用,不要在循环或分支结构中定义hook

模拟生命周期

react生命周期大概分为三种,mountupdateunmount,当然在类式组件,我们已经可以规定是在mount之前,mount时还是mount之后调用,在函数式组件我们就不分的这么清楚了,因为在函数式组件我们的生命周期其实是独立的,是面向状态的。

下面我们就使用useEffect简单模拟一下类式组件的生命周期:

只在初始化时执行(DidMounted)

1
2
3
4
5
// 不依赖任何变量,则传入一个空数组
useEffect(() => {
console.log("init");
return () => {};
}, []);

每次更新时执行(render)

1
2
3
4
// 依赖所有变量,则不传入
useEffect(() => {
console.log("rerender");
});

每次卸载时执行(beforeUnmount)

1
2
3
4
// 不依赖任何变量,return的函数将会在卸载之前被调用
useEffect(() => {
return () => {console.log("unmount");};
}, []);

实际上,我们经常说useEffect的作用是模拟声明周期,而这似乎违背了函数式组件的初衷,也违背了useEffect的初衷。我们应该多从状态的角度去思考,useEffect并非是什么生命周期函数,而是状态更新函数。

控制更新

如果某个变量在每次组件更新的时候都需要更新,那我们完全没有必要使用useEffect,我们使用副作用钩子一定是因为我们需要控制某个变量的更新。

我们可以使用useEffect组件内部的状态与组件外部系统同步

1
2
3
4
5
6
7
8
// 比如说,我们可以在父组件更新props的时候调用useEffect
const Child = ({ count }) => {
const [childContext, setChildContext] = useState(0);
useEffect(() => {
setChildContext(count);
}, [count]);
return <p>this is a child, count = {childContext}</p>;
};

或者我们可以使用useEffect组件组件外部系统与内部的状态同步

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
import "./styles.css";
import { useEffect, useState } from "react";

const fetch = async (count) => {
// 假设这是一个网络请求
// 当然,这个外部状态也可以是一个外部组件,外部变量,jsAPI等等
await new Promise(() => {
setTimeout(() => {
console.log("fetch date");
}, count);
});
};

export default function App() {
const [count, setCount] = useState(0);
const onClick = () => {
setCount(count + 1);
};
// 每次修改count都会将更改同步外部系统
useEffect(() => {
fetch(count);
}, [count]);
return (
<div className="App">
<button onClick={onClick}>Change state in father</button>
<p>count is {count}</p>
</div>
);
}

其实生命周期也不过是对状态的统一更改,react现在只是将生命周期将颗粒度从组件降低到了状态。

一些tips

在useEffect中使用state

有时候我们可能会需要在useEffect中使用和修改state,但是这是极端危险的行为,因为当我们修改state时会触发页面的rerender,然后因为useEffect依赖了state,这将会导致state被再次更改继而引起死循环……

1
2
3
4
5
6
7
8
const Child = ({ count }) => {
const [childContext, setChildContext] = useState(0);
useEffect(() => {
setChildConte(childContext);
}, [count, childContext]);
// !! 千万不要这么做,会引发死循环
return <p>this is a child, count = {childContext}</p>;
};

如果我们在useEffect中根据之前的state更新state,我们可以这么写:

1
2
3
4
useEffect(() => {
setChildContext(c => c + 1);
}, [count]);
// 不需要依赖childContext,安全

如果你真的既要在Effect中读取state,又要修改state,那么我给出的建议是,:使用两个依赖同一个更新凭证useEffect

千万别这么做:

1
2
3
4
5
useEffect(() => {
setChildContext(Math.random())
console.log(childContext)
}, [count, childContext])
// 死循环!!😭

正确的做法如下:

1
2
3
4
5
6
7
useEffect(() => {
setChildContext(Math.random())
}, [count]);
useEffect(() => {
console.log(childContext)
}, [count, childContext])
// 安全😊

消除不必要的对象与函数依赖

如果我们在组件内定义了一个函数或对象,我们在上一篇文章详细讲过,它每次都会被初始化。就像这样:

1
2
3
4
5
6
7
8
const childContext = {
count: 1
}
useEffect(() => {
childContext.count = count;
console.log("run effect")
// 不要这样,每次Render都会触发effect😔
}, [count, childContext]);

取而代之的是,如果你的对象或者函数只在useEffect中使用,则写在setup内部

1
2
3
4
5
6
7
8
9
10
11
const Child = ({count}) => {
useEffect(() => {
const childContext = {
count: 1
}
childContext.count = count;
console.log("run effect")
}, [count]);
// 推荐,只有在依赖更新的时候才会触发effect 😊
return <p>this is a child</p>;
};

如果你是希望有一个在整个组件都可以被访问的静态变量,那么可以将对象写在组件外部:

1
2
3
4
5
6
7
8
9
10
11
const childContext = {
count: 1
}
const Child = ({count}) => {
useEffect(() => {
childContext.count = count;
console.log("run effect")
}, [count]);
// 推荐,只有在依赖更新的时候才会触发effect 😊
return <p>this is a child</p>;
};

useLayoutEffect

我们在使用useEffect的时候有时会出现一种问题:我们的组件会发生闪烁或突然的变化。

这时因为useEffect是在mount之后执行的,因此第一次渲染出的值是我们定义的初始值,如果更新的性能比较差或者更新的范围比较大,可能会导致我们的view发生闪烁的现象。

这个时候我们就可以使用useLayoutEffect,他和useEffect的最大区别就是他是在mount之前执行的,所以不会导致view的闪烁。

但是如果我们没有很明显的ui闪烁问题,我们应该尽可能的使用useEffect,因为它的运行效率更好。


全面理解useEffect——useEffect用法解析与使用技巧
2023/10/02/technology/react/use_effect/
作者
charlesix59
发布于
2023年10月2日
许可协议