人人都会AI编程

23.3 副作用管理原则:依赖项完整、清理函数规范

更新时间:2026-07-11

React 中的副作用(Side Effects)是指组件渲染之外需要执行的操作,例如数据请求、订阅、定时器、手动修改 DOM 等。useEffect 是处理副作用的主要钩子,但使用不当会引发难以排查的 bug。掌握两个核心原则——依赖项完整清理函数规范,能帮助你写出健壮且可预测的代码。

依赖项完整:让副作用与状态保持同步

useEffect 的第二个参数是一个依赖数组,React 会在每次渲染后比较数组中的值是否变化,只有变化时才重新执行副作用。依赖项必须包含副作用中使用到的、且会随时间变化的外部变量(props、state、context 等)。遗漏依赖项会导致副作用读取到过期的闭包值,产生不可预期的结果。

❌ 错误示例:遗漏依赖

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, []); // 空数组表示只在挂载时执行一次
  // ...
}

userId 变化时,副作用不会重新执行,导致展示的一直是首次传入的用户信息。正确的做法是将 userId 加入依赖:

useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(setUser);
}, [userId]);

实用建议:

  • 启用 ESLint 的 react-hooks/exhaustive-deps 规则,它会自动检测缺失的依赖,这是最可靠的安全网。
  • 不要刻意回避依赖:如果你需要执行一个只希望在挂载时运行的副作用(如初始化全局事件),而依赖项会导致重复执行,那通常说明你的逻辑需要重构——或使用 useRef 存储不变值,或将副作用拆分成多个 useEffect
  • 函数依赖:如果副作用中调用了组件内定义的函数,直接写函数名会导致每次渲染都重新执行。此时可用 useCallback 包裹函数,确保引用稳定,或将该函数移入 useEffect 内部。

正确使用 useCallback 避免不必要的重复执行:

const fetchUser = useCallback((id) => {
  return fetch(`/api/users/${id}`).then(res => res.json());
}, []);

useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId, fetchUser]);

清理函数规范:避免内存泄漏与竞态条件

useEffect 可以返回一个清理函数,React 会在组件卸载或副作用重新执行前调用它。清理函数的核心作用是撤销上一次副作用的影响,常见场景包括:

  • 清除定时器
  • 取消订阅
  • 取消未完成的异步请求(或忽略其回调)
  • 移除手动添加的事件监听

❌ 常见反模式:忽略清理导致内存泄漏

useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  // 没有返回清理函数,组件卸载后定时器仍在运行
}, []);

✅ 添加清理:

useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);

处理异步请求的竞态条件:

当依赖项快速变化时,多个异步请求可能并发返回,旧请求的结果可能会覆盖新请求。清理函数可以通过一个标志变量或 AbortController 来取消过期请求:

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

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

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

事件订阅清理:

useEffect(() => {
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

实战:组织 useEffect 时的思维检查清单

  1. 问自己:这个副作用依赖哪些外部变量?

把用到的所有 props、state、context 值都列进依赖数组。

  1. 问自己:副作用是否产生了长期存在的资源?

比如定时器、订阅、WebSocket 连接、手动 DOM 事件——必须返回清理函数。

  1. 问自己:依赖变化时是否需要先清理再重新执行?

例如根据 userId 获取用户数据,切换用户时应当取消上一次的请求或丢弃其结果。

  1. 避免在 useEffect 中直接使用未在依赖中声明的异步函数:将异步逻辑包装在内部定义函数,确保依赖明确。
  1. 多个不相关的副作用应该拆分成独立的 useEffect,而不是堆在一个大的钩子里。每个 useEffect 管理自己的依赖和清理,逻辑更清晰。

遵循这两个原则能让你规避 90% 以上的副作用相关问题,也是面试中高频考察的考点。记住:副作用是必要的“脏活”,但 React 的模型要求你以可预测和可清理的方式去做。