在 React 函数组件中,
useEffect是管理副作用的核心工具,它通过统一接口替代了类组件中分散的生命周期方法(如componentDidMount和componentDidUpdate)。本文将深入解析
useEffect的原理与实践,从依赖项控制到清理逻辑,结合代码示例逐步讲解。
一、useEffect 和副作用的核心概念
在 React 函数组件中,useEffect 是一个核心 Hook,用于处理副作用操作(Side Effects)。
副作用: 指的是那些直接影响外部状态的操作,这些操作会改变函数之外的世界,比如修改全局变量、执行 API 请求、操作 DOM 或触发事件等,它们与函数本身的返回值无关,但会带来额外的影响。
在 React 中,副作用通常发生在组件的生命周期中,例如:
- 组件挂载后(如初始化数据、绑定事件)
- 组件更新后(如重新获取数据、调整 DOM)
- 组件卸载前(如清除定时器、解绑事件)
而常见的副作用通常包括以下几种情况 :
- 数据获取(如 API 请求)
- 订阅事件(如 WebSocket 监听)
- 手动操作 DOM(如设置焦点或动画)
- 清理资源(如取消订阅或清除定时器)
在类组件中,这些操作需要分散到多个生命周期方法中实现,而 useEffect 通过统一接口,将这些逻辑集中管理,使代码更简洁且易于维护。
二、useEffect 的基本用法
1. 语法结构
useEffect(effect, dependencies);
effect:一个函数,包含需要执行的副作用逻辑。它可以返回一个清理函数(用于资源释放)。dependencies:依赖项数组。React 会比较当前依赖项与上一次的值,决定是否重新执行effect。
2. 无依赖项的 useEffect
当依赖项数组为空([])时,useEffect 仅在组件挂载后执行一次,类似类组件的 componentDidMount。
useEffect(() => {
console.log("组件挂载完成");
}, []);
适用场景:初始化操作(如请求数据、绑定全局事件)。
3. 依赖项控制执行时机
通过调整依赖项数组,我们可以精确控制 useEffect 的执行时机。
(1)依赖单个状态
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count 更新为 ${count}`);
}, [count]);
这段代码通过 useEffect 监听 count 状态的变化。每当 count 被更新时,控制台会输出当前值。组件首次挂载时,useEffect 也会立即执行一次(因为首次渲染后会触发副作用)。
-
执行时机:
- 首次渲染:组件挂载后立即执行一次。
- 后续渲染:每当
count的值发生变化时(例如点击按钮调用setCount(count + 1)),副作用会被重新执行。
-
原理:React 会浅比较
count的当前值与上一次的值。如果值不同,则触发副作用。
(2)依赖多个状态
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
useEffect(() => {
console.log("count 或 num 发生变化");
}, [count, num]);
这段代码通过 useEffect 监听 count 和 num 两个状态的变化。每当这两个状态中的任意一个发生变化时,控制台会输出提示信息。组件首次挂载时,副作用也会立即执行一次。
-
执行时机:
- 首次渲染:组件挂载后立即执行一次。
- 后续渲染:当
count或num的值发生变化时(例如点击按钮调用setCount(count + 1)或setNum(num + 1)),副作用会被重新执行。
-
原理:React 会分别对
count和num进行浅比较。如果任意一个值变化,则触发副作用。
(3)依赖对象或数组
当依赖项是对象或数组时,需要特别注意引用变化的问题。
const [user, setUser] = useState({ name: "Alice", age: 25 });
useEffect(() => {
console.log("user 发生变化");
}, [user]);
这段代码通过 useEffect 监听 user 对象的变化。当 user 的引用发生变化时(例如调用 setUser 修改对象),控制台会输出提示信息。然而,如果 user 的属性值发生变化但引用未变(例如直接修改对象的属性),副作用不会触发。
-
执行时机:
- 首次渲染:组件挂载后立即执行一次。
- 后续渲染:当
user的引用发生变化时(例如调用setUser({ name: "Bob", age: 30 })),副作用会被重新执行。
-
问题:如果直接修改
user的属性(例如user.name = "Bob"),由于对象的引用未变,React 无法检测到变化,副作用不会触发。 -
原理:React 使用浅比较来判断对象或数组是否变化。只有当引用地址变化时才会触发副作用。
三、清理副作用资源
useEffect 返回的函数会在组件卸载前执行,用于清理副作用产生的资源(如取消订阅、清除定时器)。
示例:清理定时器
useEffect(() => {
const timer = setInterval(() => {
console.log("每隔 1 秒执行");
}, 1000);
return () => {
clearInterval(timer); // 清理定时器
};
}, []);
执行流程:
- 组件挂载时启动定时器。
- 组件卸载时自动调用清理函数,停止定时器。
四、useEffect 与生命周期方法的对应关系
| React Hook | 类组件生命周期方法 | 说明 |
|---|---|---|
useEffect(() => {...}, []) | componentDidMount | 仅在挂载后执行一次 |
useEffect(() => {...}, [A]) | componentDidUpdate | 在挂载后和依赖项 A 变化后执行 |
useEffect(() => {...}, []) | componentWillUnmount | 清理函数在卸载时执行 |
五、常见应用场景
1. 数据获取(API 请求)
const [repos, setRepos] = useState([]);
useEffect(() => {
fetch("https://api.github.com/users/yunan479/repos")
.then((res) => res.json())
.then((data) => setRepos(data));
}, []);
这段代码通过 useEffect 在组件挂载后向 GitHub API 发送请求,获取指定用户的所有仓库(repositories),并将结果存储在组件的 repos 状态中,由于依赖项数组为空([]),该请求仅在组件首次加载时执行一次,避免了重复调用。
逐句分析:
- 使用
useState初始化一个名为repos的状态变量,初始值为空数组。setRepos是更新该状态的方法。 - 当 GitHub API 返回数据后,
setRepos(data)会将数据存入repos,并触发组件的重新渲染。 fetch(...):向 GitHub API 发送 HTTP 请求,获取用户yunan479的仓库列表。.then((res) => res.json()):将响应内容解析为 JSON 格式。.then((data) => setRepos(data)):将解析后的数据赋值给repos状态。
2. 订阅事件
useEffect(() => {
const handleResize = () => {
console.log("窗口大小变化");
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
这段代码通过 useEffect 监听浏览器窗口的 resize 事件,当窗口大小变化时,控制台会输出提示信息,组件卸载时,会自动移除事件监听器,避免内存泄漏。
逐句分析:
handleResize函数:定义事件回调逻辑,打印窗口大小变化的信息。window.addEventListener("resize", handleResize):将handleResize绑定到resize事件。- 清理函数
return () => {...}:在组件卸载前移除事件监听器,防止事件回调在组件已卸载后仍被调用(内存泄漏)。 - 依赖项数组
[]:确保副作用仅在组件挂载时绑定一次事件监听器。
3. 条件执行副作用
通过依赖项数组,可以实现条件化逻辑。例如,仅当某个状态满足条件时才执行副作用。
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
if (searchTerm.length > 2) {
// 执行搜索逻辑
}
}, [searchTerm]);
这段代码通过 useEffect 实现一个搜索逻辑,当用户输入的 searchTerm 长度超过 2 时,才会触发搜索操作(例如调用搜索 API),通过依赖项数组 [searchTerm],确保副作用仅在 searchTerm 变化时执行。
逐句分析:
- 定义一个名为
searchTerm的状态变量,用于存储用户输入的搜索关键词。 - 条件判断
if (searchTerm.length > 2):确保仅当用户输入超过 2 个字符时才执行副作用。 - 依赖项数组
[searchTerm]:每当searchTerm变化时,副作用会被重新执行。 - 注释中的
// 执行搜索逻辑:可以替换为实际的 API 调用或其他逻辑(例如防抖处理)。