引言
你是否也遇到这些问题?
为什么我的数据请求总是重复发送?
组件卸载后,定时器还在运行,导致报错?
useEffect 的依赖数组到底该怎么写?
如果你在学习 React 时也有类似困惑,别担心,本文将带你一步步搞懂 useEffect!
一、React Hooks 基础回顾
在 React 16.8 之后,函数组件也能拥有状态和副作用管理能力,这都得益于 Hooks。最常用的有:
- useState:让组件拥有自己的状态
const [count, setCount] = useState(0); - useEffect:处理副作用(如数据请求、事件监听、DOM 操作等)
- useContext、useReducer、useCallback 等
二、什么是 useEffect?为什么要用它?
1. 副作用的概念
副作用(Side Effect)指的是函数执行时对外部环境产生影响的操作,比如:
- 发送网络请求
- 操作 DOM
- 订阅/解绑事件
- 启动/清除定时器
这些操作不直接影响组件的 UI,但会影响外部世界。
2. useEffect 的作用
useEffect 是 React 官方推荐的副作用管理方案。它让你可以:
- 在合适的时机执行副作用,避免阻塞 UI
- 精确控制副作用的执行频率
- 清理副作用,防止内存泄漏
三、useEffect 的基本用法与四大模式
基本语法
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑(可选)
};
}, [依赖项]);
1. 最基础:每次渲染后都执行
场景说明:
这是 useEffect 最简单的用法。无论组件是首次渲染还是后续更新,副作用函数都会被执行。常用于调试、日志输出等场景。
代码示例:
useEffect(() => {
console.log('组件渲染或更新时都会执行');
});
要点:
- 没有依赖数组,意味着每次渲染(包括初次和每次更新)都会执行。
- 适合需要追踪所有渲染的场景。
2. 进阶:仅在组件挂载和卸载时执行
场景说明:
有些副作用只需要在组件首次出现时执行一次(如初始化数据、注册全局事件),并在组件销毁时清理(如移除事件监听)。
代码示例:
useEffect(() => {
console.log('组件挂载时执行');
return () => {
console.log('组件卸载时执行');
};
}, []);
要点:
- 依赖数组为空,副作用只在组件挂载和卸载时执行一次。
- 常用于事件监听、定时器初始化等场景。
3. 实用:依赖特定状态变化时执行
场景说明:
当你只想在某个状态(如 count)变化时执行副作用,比如根据用户输入请求数据、更新页面标题等。
代码示例:
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `当前计数:${count}`;
console.log(`count 变化为:${count}`);
return () => {
console.log('count 即将变化或组件卸载');
};
}, [count]);
要点:
- 依赖数组中写入 count,只有 count 变化时才会执行副作用。
- 返回的清理函数会在 count 变化前或组件卸载时执行。
4. 深入:副作用的清理与资源管理
场景说明:
有些副作用需要在组件卸载或依赖变化时进行清理,比如定时器、事件监听等,否则可能导致内存泄漏或重复注册。
代码示例:
useEffect(() => {
const handleResize = () => {
console.log('窗口大小变化');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
console.log('移除窗口大小监听');
};
}, []);
要点:
- 在副作用函数中注册事件监听或定时器。
- 在返回的清理函数中移除监听或清除定时器,保证资源被正确释放。
- 依赖数组为空,表示只在挂载和卸载时处理。
综合案例
下面给出一个完整的例子的代码来演示 useEffect 的四种典型用法,并通过不同的状态和交互,帮助你理解副作用的触发时机和清理机制。
import React, { useState, useEffect } from 'react';
function UseEffectExamples() {
const [count, setCount] = useState(0);
const [data, setData] = useState('初始数据');
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
// 模式1: 每次渲染后都执行
useEffect(() => {
console.log('【模式1】组件渲染完成或更新后执行');
}); // 没有依赖数组
// 模式2: 仅在挂载时执行一次
useEffect(() => {
console.log('【模式2】组件挂载时执行 (仅一次)');
return () => {
console.log('【模式2】组件卸载时执行');
};
}, []); // 空依赖数组
// 模式3: 特定状态变化时执行
useEffect(() => {
console.log(`【模式3】count值变化时执行,当前count: ${count}`);
// 这里可以添加count变化时需要执行的逻辑
document.title = `当前计数: ${count}`;
return () => {
console.log('【模式3】count即将变化,或组件卸载时执行');
};
}, [count]); // 依赖count
// 模式4: 清理副作用的示例 (窗口大小监听)
useEffect(() => {
console.log('【模式4】设置窗口大小监听器');
const handleResize = () => {
setWindowWidth(window.innerWidth);
console.log('窗口大小变化:', window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
console.log('【模式4】移除窗口大小监听器');
window.removeEventListener('resize', handleResize);
};
}, []); // 空依赖数组,只设置一次
return (
<div style={{ padding: '20px', border: '1px solid #ccc', margin: '20px' }}>
<h2>useEffect 四种使用模式示例</h2>
<div style={{ marginBottom: '20px' }}>
<h3>模式1: 每次渲染后都执行</h3>
<p>查看控制台输出</p>
</div>
<div style={{ marginBottom: '20px' }}>
<h3>模式2: 仅挂载时执行一次</h3>
<p>查看控制台初始化和卸载时的输出</p>
</div>
<div style={{ marginBottom: '20px' }}>
<h3>模式3: count变化时执行 (当前count: {count})</h3>
<button onClick={() => setCount(c => c + 1)}>增加count</button>
<p>查看控制台和页面标题的变化</p>
</div>
<div style={{ marginBottom: '20px' }}>
<h3>模式4: 清理副作用示例</h3>
<p>当前窗口宽度: {windowWidth}px</p>
<p>尝试改变浏览器窗口大小,查看控制台输出</p>
</div>
<div>
<h3>数据变化示例</h3>
<input
value={data}
onChange={(e) => setData(e.target.value)}
placeholder="输入数据观察模式1的执行"
/>
</div>
</div>
);
}
export default UseEffectExamples;
首先,组件通过 useState 定义了三个状态变量:
count:用于计数,初始值为 0。data:用于保存输入框的内容,初始值为“初始数据”。windowWidth:用于记录当前窗口的宽度,初始值为window.innerWidth。
这些状态分别用于演示不同的副作用场景。
然后,组件内部依次设置了四个 useEffect,分别对应四种常见的副作用处理模式:
-
首先,第一个
useEffect没有依赖数组,因此每次组件渲染(包括初次和每次更新)都会执行。它在控制台输出“【模式1】组件渲染完成或更新后执行”,用于演示无依赖时副作用的触发时机。 -
接着,第二个
useEffect的依赖数组为空([]),所以只会在组件挂载时执行一次,并在组件卸载时执行返回的清理函数。它分别在挂载和卸载时输出“【模式2】组件挂载时执行 (仅一次)”和“【模式2】组件卸载时执行”。这种模式常用于只需初始化一次或全局事件注册的场景。 -
其次,第三个
useEffect依赖于count,只有当 count 发生变化时才会执行。它会在控制台输出当前 count 的值,并同步更新页面标题为“当前计数: [count]”。此外,每次 count 变化前或组件卸载时,还会执行清理函数,输出“【模式3】count即将变化,或组件卸载时执行”。这种模式适合响应特定状态变化的副作用。 -
最后,第四个
useEffect主要用于注册和清理全局事件监听。它在组件挂载时为窗口注册resize事件监听器,每当窗口大小变化时,更新windowWidth并输出新宽度。组件卸载时,清理该事件监听器,防止内存泄漏。相关输出分别为“【模式4】设置窗口大小监听器”和“【模式4】移除窗口大小监听器”。
就这样,组件的渲染部分将这四种模式的效果分别展示出来:
- 通过不同的
<div>区块,分别介绍每种模式的作用和观察方式。 - “增加count”按钮用于触发
count的变化,便于观察模式3的效果。 - 输入框用于修改
data,每次输入都会导致组件重新渲染,从而触发模式1。 - 当前窗口宽度的显示和窗口大小变化的监听,直观展示了模式4的应用。
总结
文章结构为:
-
明确副作用的本质
副作用是指那些会影响组件外部世界的操作,如数据请求、事件监听、定时器等。React 推荐用 useEffect 统一管理这些副作用,保证组件行为的可控性和健壮性。 -
掌握 useEffect 的基本语法与四大模式
- 每次渲染后都执行(无依赖数组)
- 仅挂载和卸载时执行(空依赖数组)
- 依赖特定状态变化时执行(依赖数组含状态)
- 清理副作用,防止内存泄漏(返回清理函数)
-
通过综合案例加深理解
通过一个完整的代码示例,直观展示了 useEffect 在不同场景下的用法和触发时机。你可以通过按钮点击、输入内容、调整窗口大小等操作,亲自体验副作用的执行与清理过程。
`