人人都会AI编程

useEffect:副作用处理、依赖项、清理函数

更新时间:2026-07-10

useEffect 是 React 中最核心的 Hook 之一,用于在函数组件中处理副作用——那些不属于 UI 渲染本身、但需要在组件生命周期特定时机执行的操作。

什么是副作用

副作用是组件渲染之外需要执行的逻辑,典型场景包括:

  • 数据获取(发起网络请求)
  • 订阅/取消订阅(WebSocket、事件监听)
  • 手动修改 DOM
  • 设置定时器(setTimeout/setInterval)
  • 日志记录、埋点

函数组件本身应该是纯函数——给定相同的 props 和 state,应该返回相同的 UI。副作用打破了这个纯粹性,但却是真实应用不可或缺的部分。useEffect 提供了安全、声明式的方式来编排这些副作用。

基础语法

useEffect(() => {
  // 副作用逻辑(在渲染之后执行)

  return () => {
    // 可选的清理函数(组件卸载或依赖变化前执行)
  };
}, [依赖项数组]);
  • 第一个参数:副作用函数,在组件渲染到屏幕之后异步执行,不会阻塞浏览器绘制。
  • 第二个参数:依赖项数组,决定副作用何时重新执行。
  • 返回值:清理函数,在组件卸载或依赖项变化、副作用重新执行之前调用。

依赖项的核心作用

依赖项数组控制着副作用的执行时机,三种常见用法:

1. 不传依赖项:每次渲染都执行

useEffect(() => {
  console.log('组件每次更新后都会执行');
});

极少使用,因为通常不需要在每次渲染都触发副作用,容易造成性能浪费或无限循环。

2. 空依赖数组 []:仅在挂载时执行一次

useEffect(() => {
  fetch('/api/user').then(res => res.json()).then(setUser);
}, []);

模拟类组件中的 componentDidMount,适合初始化数据请求、添加全局事件监听等只需执行一次的操作。

3. 指定依赖:依赖变化时执行

useEffect(() => {
  document.title = `未读消息:${unreadCount}`;
}, [unreadCount]);

只有当 unreadCount 发生变化时,才重新执行副作用。React 使用 Object.is 进行浅比较,因此依赖项中的对象或数组需要保持引用稳定(配合 useMemo/useCallback 或使用基本类型)。

关键原则:依赖项必须诚实地包含副作用内部使用的所有响应式值(props、state、以及由它们派生的变量)。遗漏依赖是 bug 的常见来源,而引入了不必要的变化依赖又会导致副作用频繁执行。

如果安装了 ESLint 插件 eslint-plugin-react-hooks,它会帮你检查遗漏的依赖,并在控制台提示。

清理函数的典型场景

清理函数用于防止内存泄漏,以及在依赖变化时撤销上一次的副作用。常见的清理场景:

清除定时器

useEffect(() => {
  const timer = setInterval(() => {
    setSeconds(s => s + 1);
  }, 1000);

  return () => clearInterval(timer);
}, []);

不清理会使定时器在组件卸载后继续执行,导致内存泄漏或更新已卸载组件的状态。

取消订阅 / 解绑事件

useEffect(() => {
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);

  return () => window.removeEventListener('resize', handleResize);
}, []);

每次 effect 重新执行前,会先运行上一次的清理函数,保证事件监听不会重复绑定。

取消未完成的请求(竞态处理)

useEffect(() => {
  let cancelled = false;

  fetch(`/api/user/${userId}`)
    .then(res => res.json())
    .then(data => {
      if (!cancelled) setUser(data);
    });

  return () => {
    cancelled = true;
  };
}, [userId]);

userId 快速变化时,旧请求的响应可能在新请求之后才返回,导致界面显示错误的数据。通过清理标记可以忽略过期的响应。现代更推荐使用 TanStack Query 等库自动处理这类问题。

取消订阅外部状态

useEffect(() => {
  const unsubscribe = store.subscribe(() => {
    setCount(store.getState().count);
  });

  return unsubscribe;
}, []);

常见陷阱与最佳实践

1. 避免在 effect 中执行同步状态更新导致无限循环

如果在 effect 中无条件地更新某个状态,而该状态又是该 effect 的依赖项,就会陷入无限循环。

// ❌ 会导致无限循环
useEffect(() => {
  setCount(count + 1);
}, [count]);

确保状态更新仅在特定条件下发生,或不在依赖项中包含该状态。

2. 使用多个 effect 分离不同逻辑

不要把不相关的副作用聚合在一个 effect 里:

// ✅ 好:各自独立
useEffect(() => {
  document.title = title;
}, [title]);

useEffect(() => {
  fetchData(userId);
}, [userId]);

这样每个 effect 的依赖更清晰,逻辑更内聚,减少 bug 并提高可维护性。

3. 使用 useRef 保存不需要触发重渲染的值

如果在 effect 中需要某些变量但不想把它们加入依赖项,可以将其存入 ref

const latestProps = useRef(props);
latestProps.current = props;

useEffect(() => {
  const timer = setInterval(() => {
    console.log(latestProps.current.someValue);
  }, 1000);
  return () => clearInterval(timer);
}, []); // 空依赖,但能始终访问最新的 props

但请谨慎使用,通常更推荐遵循依赖规则。

4. 清理函数中的闭包问题

清理函数捕获的是定义它时的那一轮的 props 和 state,而不是最新的值。如果需要访问最新值,可以用 ref 的方式,或者重新设计清理逻辑。

总结

  • useEffect 用于在函数组件中安全地运行副作用。
  • 依赖项数组是精确控制执行时机的核心,必须诚实声明。
  • 清理函数用于防止内存泄漏和逻辑错误,是开发稳健应用的保障。
  • 遵循“一个 effect 一件事”的原则,能显著提升代码质量。

掌握 useEffect 的精髓不在于记忆 API,而在于培养对副作用生命周期的敏感度——知道何时启动、何时撤销,这是 React 开发者从入门到进阶的关键一步。