useEffect : Hook 中的副作用统一管理

81 阅读7分钟

在 React 函数组件中,useEffect 是管理副作用的核心工具,它通过统一接口替代了类组件中分散的生命周期方法(如 componentDidMountcomponentDidUpdate)。

本文将深入解析 useEffect 的原理与实践,从依赖项控制到清理逻辑,结合代码示例逐步讲解。

一、useEffect副作用的核心概念

在 React 函数组件中,useEffect 是一个核心 Hook,用于处理副作用操作(Side Effects)。

副作用: 指的是那些直接影响外部状态的操作,这些操作会改变函数之外的世界,比如修改全局变量、执行 API 请求、操作 DOM 或触发事件等,它们与函数本身的返回值无关,但会带来额外的影响。

在 React 中,副作用通常发生在组件的生命周期中,例如:

  1. 组件挂载后(如初始化数据、绑定事件)
  2. 组件更新后(如重新获取数据、调整 DOM)
  3. 组件卸载前(如清除定时器、解绑事件)

而常见的副作用通常包括以下几种情况 :

  1. 数据获取(如 API 请求)
  2. 订阅事件(如 WebSocket 监听)
  3. 手动操作 DOM(如设置焦点或动画)
  4. 清理资源(如取消订阅或清除定时器)

在类组件中,这些操作需要分散到多个生命周期方法中实现,而 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 监听 countnum 两个状态的变化。每当这两个状态中的任意一个发生变化时,控制台会输出提示信息。组件首次挂载时,副作用也会立即执行一次。

  • 执行时机

    • 首次渲染:组件挂载后立即执行一次。
    • 后续渲染:当 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); // 清理定时器
  };
}, []);

执行流程

  1. 组件挂载时启动定时器。
  2. 组件卸载时自动调用清理函数,停止定时器。

四、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 调用或其他逻辑(例如防抖处理)。